Plugin Dependencies
Dependencies in Tako are declared in the Manifest, not in import statements. This separation is intentional — it means plugins can depend on each other without creating Go import cycles, and it gives the framework full control over boot order.
Hard vs. Soft Dependencies
You saw these in the Basics chapter. Let's go into practical detail.
Hard Dependencies (Requires)
A hard dependency says: "I cannot function without this plugin."
var Manifest = plugin.Manifest{
ID: "search-ui",
Requires: []string{"search-engine"}, // search-engine MUST be present and healthy
}What happens if search-engine is missing or failed:
- During
checkDependencies(),search-uiis marked asfailed. search-ui'sOnInitis never called.- Any other plugin that
Requires: ["search-ui"]is also marked failed. - The failure cascades cleanly up the dependency chain.
This is safe and predictable. The rest of the app boots normally.
Soft Dependencies (Recommends)
A soft dependency says: "If this plugin exists, I work better with it — but I can function without it."
var Manifest = plugin.Manifest{
ID: "analytics",
Recommends: []string{"user-session"},
}What happens if user-session is missing:
- A warning is logged:
Plugin analytics recommends user-session, which is missing analyticsboots normally — it just won't have access to session data.
Your plugin needs to handle the "missing recommends" case gracefully:
func (p *Plugin) OnInit(ctx *tako.Context) error {
var session *SessionService
// Don't fail if session plugin isn't available
_ = ctx.Container().Make(&session)
if session != nil {
p.userID = session.CurrentUserID()
} else {
p.userID = "anonymous"
}
return nil
}Dependency-Based Service Registration
The most common pattern for plugin dependencies is sharing a service through the container:
// plugins/search-engine/lifecycle.go
func (p *SearchEnginePlugin) OnInit(ctx *tako.Context) error {
// Build and register the service
engine := NewSearchEngine()
ctx.Container().Singleton(new(*SearchEngine), engine)
return nil
}// plugins/search-ui/manifest.go
var Manifest = plugin.Manifest{
ID: "search-ui",
Requires: []string{"search-engine"}, // guarantees search-engine runs OnInit first
}
// plugins/search-ui/lifecycle.go
func (p *SearchUIPlugin) OnInit(ctx *tako.Context) error {
var engine *SearchEngine
if err := ctx.Container().Make(&engine); err != nil {
// This should never happen if Requires is declared correctly,
// but be defensive anyway
return fmt.Errorf("search-ui: search engine unavailable: %w", err)
}
p.engine = engine
return nil
}Because search-ui declares Requires: ["search-engine"], the DAG sort guarantees search-engine.OnInit runs before search-ui.OnInit. By the time search-ui tries to Make the engine, it's guaranteed to be registered.
Circular Dependency Detection
If you accidentally create a cycle:
// plugin-a Requires: ["plugin-b"]
// plugin-b Requires: ["plugin-a"] ← cycle!The DAG sort catches this at boot time before any plugin is initialized:
DAG resolution failed: circular dependency detected involving plugin: plugin-aThe entire plugin boot fails. Fix the cycle by introducing a third plugin or by having one of them use Recommends instead of Requires.
Multi-Plugin Architecture Patterns
The Service + UI Split
Split concerns into headless (service) and view plugins:
plugins/
├── data-store/ TypeHeadless — owns the data, registers service in container
├── data-table/ TypeView — resolves data-store service, registers hooks for table UI
└── data-filters/ TypeView — resolves data-store service, registers filter hooksdata-table and data-filters both Require: ["data-store"]. They don't know about each other.
The Hub Pattern
One plugin acts as a hub that others optionally connect to:
// hub plugin registers a shared registry service
type WidgetRegistry struct {
widgets []Widget
}
ctx.Container().Singleton(new(*WidgetRegistry), &WidgetRegistry{})
// other plugins Recommend: ["hub"] and register their widget if hub is present
var reg *WidgetRegistry
if err := ctx.Container().Make(®); err == nil {
reg.Register(p.widget)
}The Event-Only Pattern
Sometimes you don't need a shared service — just events:
plugin-a publishes "data:updated"
plugin-b subscribes to "data:updated" — no dependency declaration neededNo Requires, no container registration. Complete decoupling. Use this when plugin-b's reaction is entirely self-contained (no need to call methods on plugin-a).
Up next: 06 — Developer Tools — the live inspector, profiler, hot-reload watcher, and how to use them during development.
