Guided Problem
MiniDock Build 30: Gate Dependencies on the Right Readiness Condition
- Time
- 25m
- Level
- intermediate
- Artifacts
- not specified
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.
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.
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.
See what actually stuck.
Take the practice scenarios now.