When catalog and stock handlers use the same database, construct one facade and pass it to both. Composition is the synchronous code that connects these typed values and registers their lifecycle; it is not a container that resolves dependencies later. This page assumes an application coordinator and an existing router. Startup execution and resource release belong to Lifecycle and Ownership.
Share Typed Dependencies
Inside your composition callback, composer is a *application.Composer from
arkoren.dev/core/application. Assume dsn is your configured PostgreSQL
connection string and routes is a *router.Router from
arkoren.dev/core/http/router. Import database from
arkoren.dev/core/service/database and postgresdatabase from
arkoren.dev/driver/postgres/postgresdatabase.
Construct the driver and immediately register its release, before any later construction can fail:
connection := composer.Must(postgresdatabase.New(dsn))
composer.Cleanup(connection.Stop)
composer.Startup(connection.Start)
db := database.New(connection)
New validates configuration and creates a lazy pool without network I/O.
Start verifies connectivity later. The facade does not start or stop its driver;
the acquiring composition code owns that resource. Do not issue queries during
composition when connectivity has not been established.
Your application-owned mountCatalog and mountStock functions each accept
(*router.Router, *database.Database). They register handlers that borrow the
facade without managing its lifecycle:
mountCatalog(routes, db)
mountStock(routes, db)
Both receive the same concrete facade. Their signatures make the dependency visible to the compiler without tying handlers to PostgreSQL. Returning or passing a value transfers no cleanup ownership.
Stop Failed Construction
Must unwraps a successful (value, error) result. On error it aborts composition:
later construction and every startup callback are skipped. Earlier registered
cleanup runs before Start or Run returns the original error, joined with
cleanup or telemetry-finalization errors if needed. Use errors.Is or errors.As
to classify the returned cause.
This ordering matters when a server’s startup registration precedes another constructor: an invalid later constructor fails first, because no startup callback runs until all composition succeeds. Cleanup follows registration position, not the number of successful startup callbacks.
For an operation returning only an error, use Require. With an existing
migrator *migrate.Migrator and definitions []migrate.Definition, both from
arkoren.dev/core/service/database/migrate, validate registration during composition:
composer.Require(migrator.Add(definitions...))
Invalid or duplicate definitions abort construction; Add executes no SQL.
Migrations owns execution and deployment policy.
Use composer.Context() if synchronous construction needs the startup context.
Must, MustDefer, and Require are valid only during synchronous composition,
even for successful results. Return ordinary errors from startup, runners, and
cleanup. Do not recover their private abort: the first failure stays recorded,
and a factory cannot publish a result after swallowing it. Unrelated composition
panics remain programmer failures and re-panic unchanged after cleanup.
Extract Owned Construction
If multiple executables need the same database setup, extract construction and
its lifecycle registrations together. A Factory[T] is a defined callback type
that publishes one typed result:
func InventoryDatabase(dsn string) application.Factory[*database.Database] {
return func(composer *application.Composer) *database.Database {
connection := composer.Must(postgresdatabase.New(dsn))
composer.Cleanup(connection.Stop)
composer.Startup(connection.Start)
return database.New(connection)
}
}
The helper captures configuration; acquisition happens inside the returned callback. Replace the direct construction with one invocation:
db := composer.Use(InventoryDatabase(dsn))
mountCatalog(routes, db)
mountStock(routes, db)
Use invokes the callback synchronously and returns its result unchanged.
Calling it again creates another driver: there is no cache, cloning, registry,
or automatic lifecycle adoption. Nested factories share the enclosing context
and cleanup stack, not a separate lifecycle.
For several related outputs, return a small named struct and pass individual fields to consumers. Avoid a global bundle of every dependency: it hides what each consumer needs. If you use a dependency-injection container, resolve typed values during composition and choose either the container or the application to own each resource’s startup and release—never both.
Own Subscriptions
An event subscription returns an unsubscribe function rather than a fallible
stop method. At the top of composition, obtain the specific event handle, then
use MustDefer to acquire and own the subscription. Here onStarted is your
existing func(context.Context, event.Void) error callback, with event from
arkoren.dev/core/event:
events := composer.App().Events()
composer.MustDefer(events.Listen(application.Started, onStarted))
The subscription lasts until rollback or shutdown cleanup, not until the composition function returns. A failed acquisition registers no release, even if it returns one: the acquiring operation must clean up partial results itself. A successful acquisition must return a nonnil release.
For an already acquired cancel context.CancelFunc, use composer.Defer(cancel).
Defer accepts func(); Cleanup accepts func(context.Context) error. They
share one once-only, reverse-registration-order stack. Nil releases panic with
ErrNilFunc; release panics follow cleanup’s panic-to-error handling.
Cleanup removes this listener before Stopped or rollback Failed. Keep
subscriptions needed for those events outside the cleanup stack, as shown in
outcome observation.
Defer and Cleanup can also register releases inside an executing startup
callback; MustDefer cannot.
Package Registration
Use an Action when a helper registers work without producing a value. Given an
existing listener *server.Server from arkoren.dev/core/http/server, whose
acquiring owner already registered listener.Stop, this helper borrows it:
func ServeInventory(listener *server.Server) application.Action {
return func(composer *application.Composer) {
composer.Startup(listener.Start)
composer.Run(listener.Serve)
}
}
Invoke composer.Use(ServeInventory(listener)) after wiring routes. The action
adds startup and supervised serving, not a second cleanup owner. It returns no
application value; call Use as a statement and ignore its empty completion
result. Root callbacks passed directly to application Start or Run remain
ordinary func(*application.Composer) functions.
Select Optional Features
If a catalog is disabled, skip its dependency and its routes together. As an alternative to the always-on database above, assume validated configuration with these typed fields:
type CatalogSettings struct {
Enabled bool
DSN string
}
With catalog CatalogSettings, construct only when selected:
db, selected := composer.UseWhen(catalog.Enabled, InventoryDatabase(catalog.DSN))
if selected {
mountCatalog(routes, db)
}
When disabled, the callback never runs: no pool, cleanup, or catalog routes are created. Keep acquisition inside the callback. Go evaluates helper arguments before selection, so passing an already opened resource does not defer acquisition.
Selectors return (T, bool). A skipped factory returns zero and false; a selected
factory returns its value and true, even when zero or nil is legitimate. Test
the boolean for presence, not success. Construction errors abort composition;
they never become false. Selectors also accept actions; their result pair can
be ignored when no later branch needs it.
Choose Selection Policy
Use a configuration boolean for a product feature. For a developer aid, choose
debug selection when operators opt in independently of deployment, or environment
selection when deployment determines policy. Given an existing
diagnostics application.Action, choose one of these alternatives:
composer.UseDebug(diagnostics)
composer.UseIn(application.Development, diagnostics)
composer.UseInAny([]application.Environment{application.Development, application.Staging}, diagnostics)
UseDebug reads the construction-time Debug flag, which defaults to false;
environment and runtime log-level changes do not alter it. UseIn and UseInAny
match the single application environment exactly, including custom values.
UseInAny runs at most once; duplicate matches do not repeat it, and an empty
slice skips it. See environment settings
for configuration. Selection is not access control: protect
diagnostic endpoints separately.
Call Use and its selectors only during synchronous composition, never from
startup, runners, cleanup, or spawned goroutines. Invalid phases panic with
ErrInvalidPhase. Nil or typed-nil operations panic with ErrNilFunc even on
skipped branches; skipping also cannot erase a recorded composition failure.
Do not retain the composer for later container callbacks.
For every composition method, see the
application API reference.