#Abstract
Service startup expressed as a sequence of statements is correct by arrangement and degrades under maintenance, because the ordering constraints are implicit in the order of lines. This note describes startup as a declared dependency graph, gives the three properties that follow, and identifies the two classes of failure the model does not address.
#The failure mode
An initialisation function opens a database, then a cache, then a message consumer, then starts an HTTP server. The order is correct because somebody arranged it correctly. Nothing records why line nine precedes line ten.
A later change inserts a component at a plausible position. The service now accepts traffic before a dependency is ready. Under light load this is invisible, because the first request arrives after everything has settled, so the defect ships and appears under load.
#Construction
Each component declares the components it requires. The container computes a start order by topological sort, starts in that order, and reverses it for shutdown.
The property that matters is not that a particular order is chosen. It is that the constraint is declared next to the component that holds it, so a new component states its own requirement and the order is derived rather than maintained.
This is the module criterion applied to time . The knowledge of what a component needs belongs to that component, and a central ordered list is that knowledge stored in the wrong place.
The sort itself is unremarkable. Kahn's algorithm produces a linear extension by repeatedly removing nodes with no remaining incoming edges , and a depth-first traversal detects cycles and yields the strongly connected components that constitute them . Using the second rather than the first is worth the small extra effort, because when a cycle exists the operator wants to be told which components are in it, not merely that the graph was not acyclic.
#Starting independent components concurrently
A linear extension is one valid order and not the only one, and this is where the declared graph pays for itself beyond maintainability. Components with no dependency relation between them may start simultaneously.
1func (c *Container) Start(ctx context.Context) error {2 waves, err := c.graph.Levels() // level i depends only on levels < i3 if err != nil {4 return err // cycle, with the offending component set named5 }6 for _, wave := range waves {7 g, gctx := errgroup.WithContext(ctx)8 for _, comp := range wave {9 g.Go(func() error { return c.startOne(gctx, comp) })10 }11 if err := g.Wait(); err != nil { // one failure cancels the wave12 return err13 }14 }15 return nil16}
A hand-written sequence cannot do this safely, because the author would have to know which adjacent lines are genuinely independent, which is the knowledge the sequence failed to record in the first place. With the graph declared, independence is derivable and the concurrency is free.
On the services measured, the effect was a startup time close to the longest dependency chain rather than the sum of all components, which for a service with several slow but unrelated initialisations is a substantial difference. Startup time matters more than it used to: it bounds how quickly a deployment can roll, how quickly an instance can be replaced under autoscaling, and how long a crash loop takes to become visible.
#Three consequences
Failures name their cause. A component that cannot start reports which component it was and which dependency it was waiting on, instead of producing a nil dereference several layers below the point where the actual cause was forgotten.
Shutdown is correct without separate effort. The reverse of a derived order is a derived order. Incorrect shutdown ordering produces connections closed underneath in-flight work and consumers handing work to components that have released their resources, which is a well documented source of failure during deployment .
The order is inspectable. It can be printed with timings. A service taking eleven seconds to start becomes a question answerable by reading a list.
#Shutdown is harder than reversal
Saying that shutdown is the reverse order is true and incomplete, and the incompleteness is where the real deployment failures live.
Reversal gives the order in which components should be stopped. It does not say what stopping means, and the two meanings that matter are different: cease accepting new work, and finish work already accepted. A component that does both at once drops in-flight requests. A component that does neither until its dependencies are gone fails them loudly.
The construction therefore separates the two into distinct phases over the same derived order. Drain runs in reverse dependency order and asks each component to stop accepting new work while continuing to serve what it holds. Stop runs afterwards, in the same order, releasing resources. An HTTP server drains first because everything else depends on it having stopped admitting requests; a connection pool releases last because everything above it may still be finishing.
Both phases need a deadline, and the deadline needs to be smaller than the orchestrator's own grace period, since a process that is still draining politely when it is killed has achieved nothing over one that exited immediately. Getting this wrong produces the failure signature of connections closed underneath in-flight work and consumers handing work to components that have released their resources, which is a well documented source of trouble during deployment .
#Optional and lazy dependencies
Two refinements were needed in practice and neither is in the basic model.
An optional dependency is one a component prefers and can operate without, a metrics exporter being the usual example. Modelling it as a hard edge means a failure in something peripheral prevents the service from starting, which is the wrong trade for a component whose absence degrades observability and nothing else. Optional edges participate in ordering when the dependency is present and are dropped when it is not.
A lazy dependency is one whose first use may be long after startup. Forcing it to start eagerly extends startup for no benefit and couples the service's availability to something it may never touch. These are excluded from the startup graph and participate in shutdown only if they were ever started.
Both are places where the model's simplicity is bought with configuration, and both are places where a developer can misdeclare and get a plausible-looking system that fails under a condition nobody tested. The honest summary is that the graph removes one class of implicit knowledge and introduces a smaller class of explicit declarations that can be wrong.
#What the model does not address
Readiness over time. A component can start successfully and remain unable to serve, because a pool is empty or a cache is cold. Startup ordering and readiness reporting are separate mechanisms and both are required, which is why orchestrators distinguish liveness from readiness .
Cycles. The model detects them, which is better than deadlocking, and detection is not resolution. A cycle indicates two components contending for one decision, and breaking it is a modelling exercise .
#Evidence
The construction was implemented twice, five years and one language apart, in a Rust framework in 2021 and a Go framework in 2025. The second implementation was written from the first design without revisiting it, which is weak evidence that the model is not an artefact of one language's constraints.
It is weak because the same author wrote both. An independent implementation would be worth more, and I do not have one.
What the second implementation did establish is which parts of the first were incidental. Ordering, cycle reporting and phased shutdown transferred unchanged. The mechanism for declaring dependencies did not: the earlier version used the type system to express edges, which was elegant and did not survive contact with a language whose type system works differently. That the model transferred and the encoding did not is the useful part of the exercise.
#Relation to existing mechanisms
Dependency injection containers already build an object graph and instantiate in dependency order, so much of this is familiar. The difference is scope: a container resolves construction, and construction order is not lifecycle order. An object can be constructed cheaply and become ready expensively, and it is readiness that traffic depends on. Conflating the two is why services that use a container still ship startup ordering defects.
Orchestrators solve an adjacent problem between processes, sequencing containers and gating traffic on probes . They cannot see inside a process, so a service that reports ready before its internal components are ready has defeated the probe. The two mechanisms compose: the graph makes the internal readiness signal correct, and the orchestrator acts on it.
The general shape of the argument is one Lampson states directly, which is that a mechanism should make the normal case correct by construction rather than by care . A hand-written sequence is correct by care. It is worth noticing that this is the same argument made elsewhere about tenant predicates and about idempotent appliers, which suggests it is the actual recurring lesson rather than a property of startup in particular.
#Limitations
No quantitative claim is made here. The assertion that startup ordering is a common source of deployment failure rests on my own experience across four frameworks, and it is exactly the sort of claim that accidental difficulty tends to attach itself to . A study of deployment incidents classified by cause would test it, and I am not aware of one.
The concurrency benefit is reported as an observation on services I built, without a controlled comparison against the same services starting sequentially. The mechanism is simple enough that the direction of the effect is not in doubt; its size on any particular service depends entirely on the shape of that service's dependency graph, and no characterisation of typical shapes is offered.
Optional and lazy dependencies are described as they were implemented rather than derived from anything. Whether they are the right two refinements, or whether a single more general mechanism subsumes both, has not been worked out.
References
- [1]D. L. Parnas, “On the Criteria To Be Used in Decomposing Systems into Modules”, Communications of the ACM, vol. 15, no. 12, pp. 1053-1058, 1972doi:10.1145/361598.361623 ↗
- [2]Arthur B. Kahn, “Topological Sorting of Large Networks”, Communications of the ACM, vol. 5, no. 11, pp. 558-562, 1962doi:10.1145/368996.369025 ↗
- [3]Robert Tarjan, “Depth-First Search and Linear Graph Algorithms”, SIAM Journal on Computing, vol. 1, no. 2, pp. 146-160, 1972doi:10.1137/0201010 ↗
- [4]Michael T. Nygard, “Release It! Design and Deploy Production-Ready Software”, Pragmatic Bookshelf, 2nd edition, 2018
- [5]Brendan Burns, Brian Grant, David Oppenheimer, Eric Brewer, John Wilkes, “Borg, Omega, and Kubernetes”, ACM Queue, vol. 14, no. 1, 2016doi:10.1145/2898442.2898444 ↗
- [6]Betsy Beyer, Chris Jones, Jennifer Petoff, Niall Richard Murphy, “Site Reliability Engineering: How Google Runs Production Systems”, O'Reilly Media, 2016
- [7]Butler W. Lampson, “Hints for Computer System Design”, ACM Symposium on Operating Systems Principles (SOSP), 1983doi:10.1145/800217.806614 ↗
- [8]Frederick P. Brooks Jr., “No Silver Bullet: Essence and Accidents of Software Engineering”, Computer, vol. 20, no. 4, pp. 10-19, 1987doi:10.1109/MC.1987.1663532 ↗