skip to content

A class named ApplicationBootstrap opens a database pool, warms a cache, starts a metrics reporter and registers shutdown hooks - grouped because all of it happens at startup. Name the kind of cohesion, and argue when grouping by lifetime is the right design rather than a smell.

level: principalimportance: should knowfreq 32%

answer

  1. temporal = grouped by clock position
  2. RAII dissolves it; Rust statics never Drop
  3. Go: defer for scopes, explicit ordered shutdown for the process
  4. supervision tree = startup order as data
  5. sequence, do not do: composition root passes

basics

~20 s

Temporal cohesion: members grouped because they run at the same moment, not because they serve one job. It is a smell when the class also performs the work; it is sound when its single job IS the lifetime policy - a composition root, or an Erlang supervision tree where startup order is declared data.

solid answer

~60 s

**Temporal cohesion** - the grouping rule is "same moment", which says nothing about shared state or a shared reason to change. Whether such a class needs to exist is largely decided by the language's resource-lifetime model: - **C++ and Rust (RAII)**: acquire and release live in one type (constructor/destructor, `Drop`), and scope gives shutdown order for free - reverse of declaration. The temporal group dissolves into per-resource ownership. Rust's catch: values with `static` lifetime never run `Drop`, so process-wide singletons push an explicit shutdown group back into existence. - **Go**: `defer` puts release next to acquire in the same function, killing function-scoped temporal grouping; with no destructors, process-lifetime resources still need an ordered shutdown, usually context cancellation plus a `WaitGroup`. - **Python**: `contextlib.ExitStack` builds the release order at runtime - the temporal group becomes a data structure you push onto. - **Erlang/Elixir**: a supervisor's child specs are startup order and restart strategy as *data*. Keep the grouping when its one responsibility is lifetime and it delegates all work; split it when it contains the work.

code

rust · 8 lines
rust
fn main() {
    let pool = Pool::connect();      // acquired 1st
    let metrics = Reporter::start();  // acquired 2nd
    run(&pool, &metrics);
}   // dropped in reverse: metrics, then pool - order is free

static CACHE: OnceLock<Cache> = OnceLock::new();
// never dropped at exit; its teardown must be arranged by hand

go deeper

for a junior

Name temporal cohesion and give the rule: grouped by when they run, not by what they are about.

for a middle

Explain why the class tends to grow without limit, and contrast it with scoped resource release such as defer or a context manager.

for a senior

Bring in resource-lifetime models - RAII versus explicit close - and argue the sequencing-versus-doing line, including the mirrored-teardown risk.

for a principal

Take a position on the architecture: make lifetime a first-class, declared structure (child specs, an ordered registry) so adding a resource is data, not logic, and state what you would give up to get there.

## Naming the grouping Temporal cohesion means the members are together because they happen at the same time. Opening a connection pool and warming a cache have no state in common, no shared subject and no common reason to change - the only thing binding them is a clock position. That is a weak binding, and the classic failure follows from it: every time someone needs "something else at startup", the class grows, because the membership rule accepts anything with the right timing. ## Why the class exists more in some languages than others The reason a bootstrap class appears at all is that the language leaves resource lifetime to the programmer. **C++ and Rust** attach lifetime to the value itself. A constructor acquires and a destructor (or `Drop`) releases; a value declared in a scope is released when the scope ends, in reverse order of declaration. This is why C++ codebases have far fewer "init everything" classes: the initialisation order is the declaration order of the members of the owning object, and the teardown order is its mirror. The dependency ordering that a bootstrap class encodes procedurally is encoded declaratively by the ownership graph. Rust adds a sharp exception: values with `'static` lifetime - a global `OnceLock`, a leaked box - are never dropped at process exit. The moment a codebase reaches for process-wide singletons, the explicit shutdown group returns, which is a nice demonstration that the class is a symptom of unowned lifetimes rather than of bad taste. **Go** has no destructors at all. `defer` solves the function-scoped case beautifully by putting the release statement immediately after the acquire, and defers run last-in-first-out, so the ordering is again free. But process-lifetime resources have no such scope, so idiomatic Go builds an explicit shutdown: a `context` that is cancelled, a `WaitGroup` that is waited on, and a hand-ordered list of `Close` calls - a temporal group by another name, deliberately written out. **Python** offers context managers for the scoped case and `contextlib.ExitStack` for the dynamic one, where you push cleanups as you acquire and the stack unwinds them in reverse. That is the same reversal rule, but as a runtime data structure rather than a static scope, which matters when the set of resources is decided at runtime. **Erlang and Elixir** take the most interesting position. A supervisor is configured with child specifications: an ordered list of children, each with a restart strategy. Startup order, dependency order and failure policy are *data*, interpreted by a supervisor whose one job is exactly that. This is the case where a temporally grouped thing is unambiguously good design, and it shows why: the grouping is not "assorted work that happens at startup", it is a single responsibility called lifetime management, with the work delegated to the children. ## The judgment call So the question is not "temporal cohesion, good or bad" but "is lifetime the responsibility, or is it the excuse". A class is doing lifetime as a responsibility when it: constructs collaborators and wires them, holds no domain logic, and its teardown is the exact mirror of its startup. A composition root - the one place in a program where the object graph is assembled - is temporally cohesive by construction and is nevertheless the recommended structure, because centralising construction is what keeps every other class free of it. A class is using lifetime as an excuse when it contains the work: the cache-warming query is written inline, the metrics reporter's configuration is parsed there, the migration runs from a private method. Now three unrelated reasons to change all point at one file, and the ordering constraints between them are invisible because they are expressed only as statement order. ## Practical tests - Can you state the class's job without the word "and"? "Owns startup and shutdown ordering for the application's resources" passes. "Opens the pool and warms the cache and starts metrics" does not. - Is teardown the mirror of startup? If the class only starts things and shutdown lives elsewhere, the ordering knowledge is duplicated in two places and will drift. - Does each started thing own its own acquire/release pair, with the bootstrap merely sequencing them? That is the RAII shape reproduced by hand, and it is what you want in a language without destructors. - Would adding a fourth resource require editing logic, or only adding an entry to a list? A list is the supervision-tree shape; logic is the god-object shape. ## The answer to give Name it as temporal cohesion, note that it is weak *as a grouping rule* but that lifetime ordering is a genuine single responsibility, and then draw the line where the work is: sequencing is a job, doing is not.

  • Is a dependency-injection composition root temporally cohesive, and is that acceptable?
    Yes and yes. It exists to run at one moment and to assemble the object graph, so its members are bound by timing. It is acceptable because its single responsibility is construction and wiring: it holds no domain logic, and centralising it is precisely what keeps construction out of every other class. The moment it starts computing something, the justification evaporates.
  • Your bootstrap class starts four resources but shutdown lives in a separate handler. What is the specific risk?
    The ordering knowledge is now duplicated in two places with no mechanism keeping them mirrored, so adding a fifth resource to startup and forgetting the corresponding teardown is a silent leak, and a wrong teardown order can close a dependency while a dependent is still using it. Languages with destructors avoid this by construction because release is derived from acquisition; without them, the mitigation is to register each release at the moment of its acquire onto a stack that unwinds in reverse.

A pre-flight checklist is temporally grouped and is exactly right, because the checklist's job is the ordering. It goes wrong the moment the checklist starts containing the maintenance procedures instead of pointing at them.

saying these in an interview costs you the question

  • Declaring all temporal cohesion a smell, so rejecting composition roots and supervision trees
  • Claiming RAII removes the need for shutdown ordering entirely, ignoring statics and process-lifetime resources
  • Assuming Go finalizers are a substitute for destructors
  • Putting the work inside the bootstrap and defending it as "it only runs once"
  • Registering startup in one class and teardown in another and calling that separation of concerns

context