fivenines
35/40

Guided Problem

MiniDock Build 30: Gate Dependencies on the Right Readiness Condition

Time
25m
Level
intermediate
Artifacts
not specified
Progress0%
Lesson 35 · Orchestration

Dependency Ordering

Design claim: Dependency edges must name the condition a consumer requires, because creation, running, health, and successful completion are different facts.

Starting model

  • You can create and update a multi-container application from one durable desired-state model.
  • The useful vocabulary at this point is deliberately small: dependency DAG, topological order, startup wave, started condition.

Order is weaker than readiness

The dispatch API must wait for a usable database, while a migration job must finish successfully before dependent traffic begins. Orchestration exists because actual state keeps changing after an initial deployment succeeds. Desired state must remain the durable reference point.

The tempting shortcut is straightforward: start dependencies first and treat process creation order as readiness. The shortcut records actions, not the condition the system is supposed to maintain. A running process may still be initializing or unhealthy, and a one-shot job provides value only after successful completion. Retries, readiness races, or host loss then produce duplicate work or unowned deficits.

Put a condition on every edge

Dependencies must form a DAG (a cycle is a config error, rejected at parse time). The engine repeatedly starts every service whose dependencies are all satisfied — meaning independent services launch simultaneously. But "satisfied" has two strengths, and the difference is where real outages hide. started (the default) waits only for the container to exist and run; a db that's "started" may still be replaying its journal and refusing connections. service_healthy waits for the dependency's health check (Lesson 24) to report Healthy — genuine readiness. The rule of thumb: gate on health wherever the depender's first action is to connect.

Dependency edges carry explicit started, healthy, or completed-successfully conditions evaluated from lifecycle and health evidence. The mechanism represents intent explicitly, derives work from the current difference, and limits authority to the component able to commit that intent.

Read the control path from durable intent to observed tasks, distinguishing authority from execution.

Architecture — a dependency DAG with parallel waves
  flowchart TB
    DB["db
(healthcheck: accepts connections)"] RD["redis"] MIG["migrate
depends_on db: service_healthy"] API["api
depends_on: migrate complete, redis started"] WEB["web
depends_on api: service_healthy"] DB --> MIG MIG --> API RD --> API API --> WEB W1["wave 1 (parallel): db, redis"] W2["wave 2: migrate"] W3["wave 3: api"] W4["wave 4: web"] style W1 fill:#eaf1fe,stroke:#1d63ed style W2 fill:#eaf1fe,stroke:#1d63ed style W3 fill:#eaf1fe,stroke:#1d63ed style W4 fill:#eaf1fe,stroke:#1d63ed

It labels dependency edges with started, healthy, and completed conditions. Its central claim is that a dependent starts only after the specific fact it requires has been observed; the labels therefore describe authority rather than decorative grouping.

Release dependents from observed facts

Dependencies must form a DAG (a cycle is a config error, rejected at parse time). The engine repeatedly starts every service whose dependencies are all satisfied — meaning independent services launch simultaneously. But "satisfied" has two strengths, and the difference is where real outages hide. started (the default) waits only for the container to exist and run; a db that's "started" may still be replaying its journal and refusing connections. service_healthy waits for the dependency's health check (Lesson 24) to report Healthy — genuine readiness. The rule of thumb: gate on health wherever the depender's first action is to connect.

The orchestrator starts prerequisites, subscribes to the required condition, releases each dependent when that condition holds, and propagates terminal failure. Each action should reduce a measured difference and remain safe when observation causes the loop to run again.

The second figure tests the same model in motion: it shows lifecycle and probe events satisfying or failing those conditions.

Sequence — health-gated startup doing its job
  sequenceDiagram
    participant C as Compose engine
    participant DB as db
    participant M as migrate
    C->>DB: start (wave 1)
    DB->>DB: Running — but replaying journal, refusing connections
    C->>C: migrate waits (gate: service_healthy, not started)
    DB->>DB: journal done → health probe passes → Healthy
    DB-->>C: health event: Healthy
    C->>M: start migrate (its first act: connect to db — succeeds)
    note over C: with the default "started" gate,
migrate would have connected into the journal replay and died

It shows lifecycle and probe events satisfying or failing those conditions. The ordering is valid only when it continues to preserve the stated invariant under retries and interruption.

A running dependency may never become usable

The database process runs but never becomes healthy, so the API must remain gated instead of mistaking start order for readiness. Failure changes observed state, not the desired result; recovery should therefore repair only the new difference.

A dependent starts only after the specific fact it requires has been observed. The rule combines explicit conditions, quorum authority, and repeatable reconciliation without making workers owners of truth.

Give MiniDock honest dependency gates

MiniDock composes lifecycle and health signals into dependency semantics without inventing one universal ready state. MiniDock can compose its existing APIs into continuous control while the design still requires a placement choice.

What carries forward

  • A dependent starts only after the specific fact it requires has been observed.
  • Start each service only after its own dependency conditions while leaving unrelated work unblocked.
  • The rejected shortcut remains a diagnostic: if the design starts depending on it again, the original constraint has probably been lost.
Next step

See what actually stuck.

Take the practice scenarios now.