Yama is a compile-time lifecycle orchestration framework for Go applications.
Yama derives lifecycle orchestration directly from a Google Wire dependency graph. It generates lifecycle execution code during build time.
The project eliminates duplication between:
- Dependency construction.
- Startup orchestration.
- Shutdown orchestration.
Yama follows the same core philosophy as Google Wire:
- Generate code.
- Avoid reflection.
- Avoid runtime registration.
- Avoid service locators.
- Avoid runtime graph construction.
- Preserve strong typing.
- Generate understandable Go code.
Yama does not implement dependency injection. It delegates dependency construction to Google Wire and implements only lifecycle orchestration.
An application that uses Yama gets both: Wire constructs the graph, and Yama orchestrates its lifecycle.
Dependency-injected applications frequently maintain the same dependency graph multiple times.
Example:
Dependency Graph
↓
Dependency Construction
Dependency Graph
↓
Startup Ordering
Dependency Graph
↓
Shutdown Ordering
This duplication creates:
- Maintenance burden.
- Lifecycle drift.
- Startup ordering mistakes.
- Shutdown ordering mistakes.
- Reduced operational confidence.
The dependency graph already contains sufficient information to derive lifecycle ordering.
Yama eliminates duplicated lifecycle definitions by generating lifecycle orchestration directly from the Wire dependency graph.
Use the Google Wire dependency graph as the authoritative source of lifecycle ordering.
Generate lifecycle orchestration during build time.
Preserve compile-time type safety.
Ensure lifecycle behavior is fully determined by generated code.
Generate code that can be:
- Read.
- Reviewed.
- Debugged.
- Stepped through.
using ordinary Go tooling.
Expose only lifecycle concepts applications must directly interact with.
Provide interceptor-based lifecycle customization without expanding lifecycle manager responsibilities.
Yama shall not provide:
Yama does not implement its own injector. Google Wire remains responsible for dependency injection.
Runtime reflection is prohibited.
Runtime registration systems are prohibited.
Service locator APIs are prohibited.
Dependency graphs are generation-time artifacts.
Lifecycle plans are not exposed at runtime.
Graph inspection APIs are prohibited.
Health management is outside project scope.
Readiness management is outside project scope.
Configuration acquisition, parsing, validation, and reload are outside project scope.
Yama is not a workflow engine.
The framework supports exactly:
- Start
- Quiesce
- Stop
No additional phases shall be introduced.
Primary users include:
- Go infrastructure engineers.
- Platform engineers.
- Backend engineers.
- Teams already using Google Wire.
Applications that do not use Google Wire are outside the primary target audience.
The framework shall support optional lifecycle capabilities.
type Starter interface {
Start(context.Context) error
}
type Quiescer interface {
Quiesce(context.Context)
}
type Stopper interface {
Stop(context.Context)
}Start returns an error. Quiesce and Stop do not. The signature of each
interface matches the error semantics of the phase it represents.
Components may implement:
- None
- Starter
- Quiescer
- Stopper
- Any combination
All Wire graph components participate in dependency analysis.
A component occupies a level in lifecycle execution in two cases. First, the component is lifecycle-capable. Second, the component's provider returned a Google Wire cleanup. A component receives a callback only for the capabilities it implements. A component that occupies a level only because of a cleanup receives no callback.
Startup shall follow dependency ordering.
A component shall not start before its dependencies have started successfully.
Independent dependency branches shall start concurrently.
Startup shall fail fast.
Startup failure shall:
- Stop scheduling startup levels. The framework does not cancel in-flight operations in the active level. It waits for them to finish.
- Initiate shutdown cleanup.
- Return a lifecycle startup failure.
Quiesce is the first pass of shutdown. A component that quiesces stops accepting new work. It then waits for its in-flight work to complete. This wait is subject to the context passed to Quiesce.
Quiesce shall:
- Execute in reverse dependency order (dependents before dependencies), the same direction as Stop.
- Execute independent branches concurrently.
- Complete before teardown begins.
Quiescer.Quiesce returns no error.
Applications shall not invoke Quiesce directly. It is not exposed on Lifecycle.
Stop runs the quiesce pass internally as its unconditional first action.
Shutdown shall execute in reverse dependency order.
A dependent shall stop before the dependency it relies upon.
Independent branches shall stop concurrently.
Shutdown shall run to completion in dependency order. The caller's context deadline is observational. The framework does not return early when the deadline expires. It logs a per-component overrun after that component's operation returns. A hung component stalls every component after it until something sends SIGKILL. The framework never reports the hung component itself. Yama chooses ordering over liveness.
If startup fails once the levels are running:
Start
↓
Failure
↓
Stop (quiesce pass, then teardown)
Startup failure cleanup shall use the same shutdown sequence as normal operation.
The framework shall generate no lifecycle configuration.
There is no generated configuration structure and no start or shutdown deadline
field. The only deadline is the one carried by the context that the caller
passes to Start and Stop. The framework threads that context through the
traversal and never lengthens its deadline. The deadline is observational. The
framework does not enforce it. The traversal continues to completion. The
framework logs a component that exceeded the deadline after that component's
operation returns.
A component that wants a per-component timeout wraps its own Start, Quiesce, or
Stop. This is ordinary Go, not a framework mechanism. Slow-operation and overrun
diagnostics are interceptor concerns.
The framework provides no configuration loading, parsing, discovery, validation, precedence, or reload.
The framework shall support lifecycle interceptors.
Interceptors shall be:
- Runtime objects.
- Operation-specific.
- Strongly typed.
- Not uniform. Each interceptor's signature matches the error semantics of the phase it wraps. Start propagates an error. Quiesce and Stop do not.
Separate interceptor chains shall exist for:
- Start
- Quiesce
- Stop
Interceptors may:
- Observe behavior.
- Modify behavior.
- Modify context.
- Suppress execution.
- Modify lifecycle outcomes.
Interceptors attach globally. An interceptor joins the chain for every lifecycle component whose operation it intercepts. There is no per-component scoping.
The lifecycle manager shall provide lifecycle metadata through context propagation.
Metadata shall support:
- Logging.
- Metrics.
- Telemetry.
- Tracing.
Metadata shall include the lifecycle component identity being started, quiesced, or stopped.
The exact metadata representation is an implementation detail.
Through interceptors, the framework shall support observation of start, quiesce, and stop execution, failures, deadline overruns, and durations. The framework shall remain agnostic to any specific observability tool.
The framework shall expose a single lifecycle-level error, ErrStartFailed.
Start is the only lifecycle operation that returns an error. Stop returns
nothing.
The framework shall not expose:
- Component errors.
- Component identity.
- Component diagnostics.
- Error aggregation structures.
Detailed diagnostics shall be provided through interceptors and observability integrations.
The framework shall generate lifecycle orchestration code during build time.
Generated code shall:
- Compile without reflection.
- Be understandable by humans.
- Be debuggable.
- Be deterministic.
Generated code shall be executable Go code rather than runtime lifecycle plans.
The framework shall support boundary components. A boundary component is a lifecycle-capable component that sits outside the Wire dependency graph. The application registers it, and it then participates in every lifecycle pass.
An application may register a component at the begin boundary or the end boundary. These two positions model the extremes of dependency order in the graph. A begin-boundary component behaves as a base dependency. It starts before every graph component. It quiesces and stops after every graph component. An end-boundary component behaves as a top dependent. It starts after every graph component. It quiesces and stops before every graph component. Shutdown is the exact reverse of startup. Within a boundary set, components have no order among themselves.
Boundary components implement the same optional lifecycle capabilities as graph components. They follow the same startup, quiesce, and shutdown semantics.
The framework shall provide a helper that runs a lifecycle until it receives an operating-system signal. The helper starts the lifecycle. It then blocks until one of a caller-specified set of signals arrives. It then shuts the lifecycle down.
When the caller specifies no signals, the helper shall run until it receives an interrupt or termination signal.
The framework shall preserve compile-time type safety.
Lifecycle execution shall be deterministic.
Generated code shall be navigable using standard IDE tooling.
Generated code shall prioritize readability over code generation cleverness.
Lifecycle analysis shall occur during generation rather than execution.
Runtime overhead shall be minimized.
Lifecycle execution shall support comprehensive observability through interceptors.
The project is considered successful when:
- Lifecycle orchestration can be generated directly from a Wire graph.
- Startup ordering is automatically derived.
- Shutdown ordering is automatically derived.
- Independent branches execute concurrently.
- No reflection is required.
- No runtime graph construction is required.
- Generated code remains readable.
- Generated code remains debuggable.
- Interceptors provide lifecycle extensibility.
- The public API stays minimal: the capability and interceptor interfaces, the
Lifecycletype,ErrStartFailed, theFromContextaccessor, theOptiontype with itsWithBeginComponents,WithEndComponents, andWithInterceptorsconstructors, andRunUntilSignal.
The project may accumulate unrelated framework responsibilities.
Large dependency graphs may produce large generated artifacts.
Interceptors may be used to implement unrelated concerns.
The project intentionally depends on Wire concepts and workflows.
Potential future enhancements include:
- Additional observability integrations.
- Lifecycle visualization tooling.
Future enhancements must preserve:
- Compile-time operation.
- Strong typing.
- Minimal public API.
- Generated-code-first philosophy.