Contracts — Composing Contract Chains
Why Composition Matters
Real features are never one contract. They are chains — sequences of contracts where each step's output feeds the next step's input. The chain's success depends on every link, and a failure at any point must be handled.
Composing contracts is where design becomes architecture.
The Three Rules of Contract Chains
Rule 1: Output Shape Must Match Input Shape
If Contract A returns a validated_cart and Contract B accepts a validated_cart, they connect. If A returns a cart (not validated) and B expects validated_cart, they don't — and the gap will produce a bug.
Rule 2: Every Link Has a Failure Path
What happens if step 3 of 5 fails? Does the whole chain stop? Do steps 1 and 2 need to be undone? Does the chain skip step 3 and continue? The answer must be explicit for every link.
Rule 3: Side Effects Complicate Rollback
If step 1 sends a confirmation email and step 3 fails, you can't "unsend" the email. Side effects are often irreversible, so the chain must account for which steps can be undone and which can't.
Worked Example 1: E-Commerce Checkout
A customer clicks "Place Order." Here's the full chain:
Step 1: ValidateCart
IN: cart_id
OUT: validated_cart (items confirmed in stock, prices confirmed current)
FAIL: "Item X is out of stock" → stop, show error, suggest alternatives
Step 2: CalculateTotal
IN: validated_cart, discount_code (optional), shipping_address
OUT: order_total (subtotal, discount_amount, tax, shipping, grand_total)
FAIL: "Invalid discount code" → stop, show error, let customer fix
FAIL: "Cannot ship to this address" → stop, show error
Step 3: ReserveInventory
IN: validated_cart
OUT: reservation_id (items held for 10 minutes)
FAIL: "Item X went out of stock since cart was validated" → stop, show
error, go back to step 1
NOTE: This is a TEMPORARY hold. If step 5 fails, reservation is released.
Step 4: ProcessPayment
IN: grand_total, payment_method
OUT: payment_confirmation (transaction_id, status)
FAIL: "Card declined" → release reservation (undo step 3), show error
FAIL: "Payment service unavailable" → release reservation, show error,
suggest retry
Step 5: CreateOrder
IN: validated_cart, order_total, payment_confirmation, customer_id
OUT: order_record (order_id, status = "confirmed")
FAIL: "System error creating order" → THIS IS CRITICAL. Payment was
already processed. Must either: (a) retry order creation, or
(b) refund the payment. Never leave money charged without an order.
Step 6: SendConfirmation
IN: order_record, customer_email
OUT: (none — fire and forget)
FAIL: "Email service unavailable" → log the failure, do NOT undo the
order. The order is valid. Email can be resent later.
SIDE EFFECT: Email sent to customer.
The Failure Cascade
Let's visualize what happens at each failure point:
| Fails At | Steps Completed | What Must Be Undone | User Sees |
|---|---|---|---|
| Step 1 | None | Nothing | "Item out of stock" — redirect to cart |
| Step 2 | Cart validated | Nothing (validation has no side effects) | "Invalid discount code" — fix and retry |
| Step 3 | Cart validated, total calculated | Nothing (no side effects yet) | "Item just went out of stock" — back to cart |
| Step 4 | Cart validated, total calculated, inventory reserved | Release inventory reservation | "Card declined" — try another card |
| Step 5 | All above + payment charged | Refund payment + release inventory | "System error — please contact support" + automatic refund |
| Step 6 | Everything above + order created | Nothing to undo — order is valid | Order succeeds. Email will be retried later. |
This table is the most valuable artifact in the design. It shows exactly what's at risk at each step and what recovery looks like.
Worked Example 2: Employee Onboarding
A new employee is onboarded into a company's systems. This is a multi-system chain involving HR, IT, Facilities, Payroll, and more.
Step 1: CreateEmployeeRecord
IN: name, role, department, start_date, manager_id, salary
OUT: employee_id, employee_record
FAIL: "Manager not found" → stop, HR fixes manager assignment
FAIL: "Duplicate employee (matching name + DOB)" → stop, HR investigates
Step 2: SetupPayroll
IN: employee_id, salary, tax_withholding_info, bank_account (for direct deposit)
OUT: payroll_enrollment_confirmation
FAIL: "Invalid bank routing number" → stop, request corrected info
(Step 1 persists — employee record exists but payroll isn't set up)
SIDE EFFECT: Employee added to next payroll cycle
Step 3: CreateIT Accounts
IN: employee_id, role, department
OUT: email_address, system_credentials, access_permissions_list
FAIL: "Email address conflict (name.lastname already taken)" →
auto-generate alternative (name.middle.lastname), proceed
SIDE EFFECTS: Email created, VPN access granted, software licenses assigned
Step 4: AssignEquipment
IN: employee_id, role, department
OUT: equipment_list (laptop model, monitor, phone, badge)
FAIL: "Laptop model out of stock" → substitute, proceed with warning
SIDE EFFECT: Equipment reserved in inventory, shipping initiated
Step 5: SetupWorkspace
IN: employee_id, department, start_date
OUT: workspace_assignment (building, floor, desk number)
FAIL: "No desks available in department area" → assign temporary desk,
add to waitlist
SIDE EFFECT: Desk reserved in facilities system
Step 6: SendWelcomePackage
IN: employee_id, email_address, start_date, workspace, equipment_list
OUT: (confirmation)
FAIL: Non-critical — retry later
SIDE EFFECT: Welcome email sent with first-day instructions
Key Differences From E-Commerce Chain
Not all steps are dependent. Steps 2, 3, 4, and 5 can happen in parallel — they all need employee_id from step 1, but they don't need each other's outputs. This changes the chain from a strict sequence to a fan-out:
┌── Step 2: Payroll
├── Step 3: IT Accounts
Step 1: Create Record ──────────┤
├── Step 4: Equipment
└── Step 5: Workspace
│
All complete ─────┘
│
Step 6: Welcome
Failures don't cascade backward. If IT can't create an account, that doesn't mean HR needs to delete the employee record. Each step has its own failure handling. This is a design choice — the chain is tolerant of partial completion, unlike the e-commerce chain where payment requires inventory reservation.
Some failures are handled with substitution, not cancellation. "Laptop out of stock" → substitute a different model. "Email conflict" → generate alternative. The chain tries to continue whenever possible.
Worked Example 3: Medical Lab Test Process
A doctor orders a blood test. The sample is collected, processed, and results are delivered.
Step 1: OrderTest
IN: doctor_id, patient_id, test_type (e.g., "complete blood count"),
urgency ("routine" | "urgent" | "stat"), clinical_notes
OUT: test_order (order_id, patient_name, test_type, collection_instructions)
FAIL: "Patient has allergy flagged for this test prep" → warning to doctor
SIDE EFFECT: Order appears on lab's work queue
Step 2: CollectSample
IN: order_id, collector_id (phlebotomist), patient_id_verification
(wristband scan or verbal confirmation of DOB)
OUT: sample_record (sample_id, collection_time, tube_type, volume)
FAIL: "Patient ID verification failed (wristband doesn't match order)"
→ HARD STOP. Do not collect. This prevents testing the wrong
patient's blood — a potentially fatal error.
FAIL: "Insufficient sample volume" → recollect
SIDE EFFECT: Sample labeled with barcode, linked to order_id
Step 3: ProcessSample
IN: sample_id
OUT: processing_record (processing_start_time, analyzer_id, status)
FAIL: "Sample hemolyzed (damaged)" → error to collector: "Recollection
needed. Reason: hemolysis." → back to Step 2
FAIL: "Analyzer malfunction" → route to backup analyzer
SIDE EFFECT: Sample processing logged for quality control
Step 4: AnalyzeResults
IN: processing_record
OUT: raw_results (values for each test component, reference ranges,
flags for abnormal values)
FAIL: "Results outside analyzable range" → flag for manual review
by lab technician
SIDE EFFECT: Results stored in lab information system
Step 5: ReviewResults
IN: raw_results, patient_history (previous test results for comparison)
OUT: reviewed_results (same as raw, plus: technician_notes,
critical_value_flag)
FAIL: None — this step always produces a result (even if the result
is "requires further testing")
SIDE EFFECT: If critical value detected (life-threatening result),
IMMEDIATE notification to ordering doctor — this must happen within
minutes, not hours. This is a regulatory requirement.
Step 6: DeliverResults
IN: reviewed_results, doctor_id, patient_id
OUT: delivery_confirmation
FAIL: "Doctor not available" → deliver to covering physician
SIDE EFFECTS: Results appear in patient's medical record,
doctor receives notification in their clinical dashboard
Why This Chain Is Unique
Patient safety creates hard stops. Step 2 has a verification check that cannot be bypassed or substituted. If the patient ID doesn't match, the chain stops completely. No alternative, no workaround. This is the highest-stakes failure in the chain — a wrong patient's blood being analyzed means wrong treatment decisions.
Some failures loop backward. "Sample hemolyzed" at step 3 sends the chain back to step 2 (recollect). This isn't a simple linear chain — it has loops.
Time sensitivity varies by step. Routine orders might wait hours at each step. Stat orders bypass the queue at every step. The urgency flag changes the behavior of every contract in the chain without changing the contracts themselves — it's a priority signal that travels through the chain.
Critical value notification is a side effect that overrides normal flow. Normally, results go through all steps sequentially. But if step 5 detects a critical value (e.g., dangerously low blood sugar), the side effect triggers an immediate alert — even before step 6 formally delivers the results. The side effect has higher priority than the main chain.
Composing Contracts: Summary Principles
| Principle | What It Means |
|---|---|
| Map the happy path first | Get the chain right when everything works, then add failure handling |
| Define the failure point for every step | What happens here if this fails? Stop? Undo? Substitute? Skip? |
| Identify irreversible steps | Emails sent, payments charged, physical actions taken — these can't be undone |
| Look for parallelizable steps | Not every chain is strictly sequential — find steps that don't depend on each other |
| Look for loops | Some failures send you back to an earlier step. Map these explicitly. |
| Time sensitivity shapes the chain | A chain that must complete in 200 milliseconds is designed very differently from one that spans 5 business days |
| The rollback plan is as important as the happy path | For every step that changes state, document how to reverse it if a later step fails |