#The usual approach
Most services start with an initialisation function. Open the database, then the cache, then the message consumer, then start the HTTP server. The order is expressed by the order of the lines, and it is correct because somebody arranged the lines correctly.
That works until the list is long enough that nobody remembers why line nine is before line ten. Someone adds a dependency, puts it in a reasonable-looking place, and the service now starts accepting traffic before a component it needs is ready. Under light load nothing happens, because the first request arrives after everything has settled.
#What North did instead
Each component declares what it depends on. The container computes a start order by topological sort, starts them in that order, and reverses it for shutdown.
Rust pushed me here rather than the other way around. Ownership made me say, explicitly, which component held which resource and for how long, and once that was written down the ordering was derivable. In a language that let me be vague I would have written the sequential version and not noticed.
#The three things it bought
Failures name the cause. When a component cannot start, the error says which component it was and which dependency it was waiting on. Compare that to a null dereference three layers down at a point where the actual cause has already been forgotten.
Shutdown is correct without effort. The reverse of a computed order is a computed order. Getting shutdown wrong is common and its symptoms are ugly: connections closed underneath in flight requests, consumers still handing work to components that have released their resources .
The graph is inspectable. You can print it. When a service takes eleven seconds to start and nobody knows why, a printed order with timings answers the question in a minute.
#Where it does not help
A dependency graph says nothing about readiness over time. A component can start successfully and still be unable to serve, because a connection pool is empty or a cache is cold. Startup ordering and readiness reporting are different mechanisms and both are needed, which is the distinction that readiness and liveness probes exist to make .
It also does not help with cycles. It detects them, which is better than deadlocking, and detection just tells you that your design has a cycle. Breaking it is a modelling problem, usually solved by finding the decision that two components are both trying to own .
#Where it went
This is the one idea that survived both Rust frameworks intact and went into everything I have built since. Of everything in those two projects, it is the piece I would defend without qualification.
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]Michael T. Nygard, “Release It! Design and Deploy Production-Ready Software”, Pragmatic Bookshelf, 2nd edition, 2018
- [3]Betsy Beyer, Chris Jones, Jennifer Petoff, Niall Richard Murphy, “Site Reliability Engineering: How Google Runs Production Systems”, O'Reilly Media, 2016