A Join Is a Decision, Not a Formality
Read11 min · Map → Contract → Route → Verify · Updated 2026-09-15
You end this lesson with: A join point defined for your fan-out, with a written reason it's needed and a partial-failure rule.
Chapters
- When a join earns its wait01
- The batch report join, done correctly02
- Partial failure is the default case, not the edge case03
- Faded case04
- Open case05
- Key takeaways06
Lecture 05. A Join Is a Decision, Not a Formality
Illustrative composite. The contract-review graph fanned out cleanly: forty contracts, four extract nodes each, all running independently. Then someone added a join after every single stage: wait for all forty fetches, then wait for all hundred-sixty extracts, then wait for all hundred-sixty checks, then write the report. Four barriers, each one waiting for every contract to reach the same line before any of them could move to the next stage.
One contract's PDF was corrupted and its fetch hung for six minutes. Every other contract (already fetched, already extracted, already checked) sat idle at the next barrier, waiting for the one that was stuck. The fan had quietly turned back into a chain, just a wider one. Parallelism was easy to add. It was just as easy to cancel out by joining too eagerly.
When a join earns its wait
A join is worth the wait only when the next node genuinely needs the complete set: not "would find it convenient," needs it.
It's needed when:
- You're deduplicating across branches. Four independent extract nodes might all flag the same clause under different names; only a join that sees all four can catch that.
- You're ranking or comparing candidates. "Which of these five contracts carries the most risk" requires all five verdicts in hand at once.
- You're deciding whether coverage is sufficient. "Did every contract in the batch get a verdict, or did three silently fail?" is a question only the full set can answer.
It's not needed when each result can move on by itself. check liability finishing for contract seventeen doesn't need to wait for contract sixteen's check to finish: nothing about write risk note (per-contract) requires the full batch. Keep that path streaming, not joined.
Only the join needs every branch; each per-contract note ships the moment its own check lands.
The batch report join, done correctly
The one place in this graph where a join genuinely earns its wait is the final batch report: "how many contracts were reviewed, how many flagged, which need escalation" is a question about the complete set, and only the complete set can answer it.
interface BatchJoin {
expected: number; // 40 — the fan-out width
settled: ContractResult[]; // results that completed, in any order
failed: ContractId[]; // branches that never settled
}
function reportOnJoin(join: BatchJoin): BatchReport {
// Don't wait forever for a branch that will never settle.
// Collect what settled, note what didn't, decide if that's enough.
const coverage = join.settled.length / join.expected;
return {
reviewed: join.settled,
incomplete: join.failed,
sufficientCoverage: coverage >= 0.95, // policy call, not a technical one
};
}
That last field is the point of this section: a join is where the graph makes a coverage decision, not just where it waits. Deciding whether enough of the batch settled to proceed — say, five failed extractions out of the whole run — is a real design choice someone has to make, and the join is where it gets made explicit instead of happening by accident.
Illustrative composite. The 95% coverage floor in the code above is a worked example for this lesson, not a recommended threshold — pick the number that matches your own batch's stakes.
Partial failure is the default case, not the edge case
At forty parallel branches, at least one failing is the common case, not the rare one: a malformed PDF, a rate limit, a clause genuinely absent from the contract. A join designed as if every branch always succeeds will hang, crash, or silently drop data the first time reality intervenes. Design the join's failure policy before running the fan, not after the first stuck branch teaches you the hard way:
- Fail-closed: any branch failure blocks the join (right for a small, high-stakes batch where every contract matters individually).
- Fail-open with a coverage floor: proceed once N% of branches settle, note the rest as incomplete (right for a large batch where a few stragglers shouldn't block the whole report).
- Fail-open with per-branch retry: give each failed branch a bounded number of retries before counting it against the coverage floor (the controlled cycle from Lecture 04, attached to the join).
Faded case
Skip to the open case if this is familiar. A separate pipeline fans out twelve invoice-matching checks per purchase order, one per line item, and writes a per-line-item note back to the vendor the moment its own check settles, the same streaming shape as the risk notes above. A teammate proposes adding one more join, after all twelve checks, right before any note goes out, so the vendor receives "a single tidy summary instead of twelve separate pings" rather than a note per line item. Nothing about the purchase order's line items depends on each other finishing in any particular order, and nothing downstream currently reads all twelve at once. Decide now: does this new join earn its wait, or does it just add a barrier where nothing downstream needs the complete set yet?
Open case
Look at a fan-out in your own workflow where you already join after every stage out of habit. Exercise 1 below asks you to redraw it — do that now, before re-reading the rule, and see how many of those joins survive the same test this lecture just walked through twice.
Key takeaways
- A join is worth the wait only when the next node needs the complete set: deduplication, ranking, or a coverage decision. Otherwise, keep the path streaming.
- Joining after every stage quietly turns a fan back into a chain. The corrupted-PDF example is the general failure mode: one stuck branch blocks every sibling that already finished.
- A join is where the graph makes a coverage decision, not just where it waits. "Enough of the batch settled to proceed" is a policy call: make it explicit in the join's design, not implicit in whatever happens to hang.
- Design the failure policy before running the fan. Fail-closed, fail-open-with-a-floor, and fail-open-with-retry are three real choices; pick one on purpose.
What You Have Now
A join point defined for your fan-out, with a written reason it's needed and a partial-failure rule. It attaches to the fan segment you labeled in Lecture 04, completing the Route stage's coordination design for that part of the graph. The failure policy you chose here is the piece Project 01 asks you to actually test.
Save it as graph/lecture-05.md; the lab's checker looks for that file at that path.
Exercises
- Find an over-joined fan. In a workflow you run, find a place where results are joined more often than the next step actually requires. Redraw it so results stream individually until a real join point.
- Write a join's failure policy. For your batch report join (or an equivalent in your own task), write down which of the three failure policies above applies, and why, in one sentence you could show a teammate.
Check your understanding
Reading is free — your progress isn't being saved
Nothing you did on this page is recorded. Paid students get saved progress and verified completions, recorded quiz answers, the spaced-repetition review queue, labs, and a certificate for each course they finish.
30-day money-back guarantee. One-time payment — lifetime access and updates.