Software Design Reference Initiative Lead · Design the System

Lesson 0045 · Initiative Lead · Module 2

Design State, Data Ownership, and Contracts

Most integration bugs are unanswered state questions wearing API syntax. Model transitions, ownership, failure, and change before polishing endpoints.

Reading ≈ 8 minPractice ≈ 45 min

Mission tie-in: data is the durable part of a system. A lead makes its invariants, writer, lifecycle, and compatibility explicit before code makes accidental promises.

Knowledge: state first, transport second

“Return status” sounds like a field. It is actually a state machine with allowed transitions and actors:

Return journey · explicit state model
FromCommandToRule / owner
—request returnpending decisionJourney creates one request per intent.
pending decisionrecord eligibilityapproved / refusedEligibility owns decision and reasons.
approvedattach labelready to shipJourney accepts one carrier label.
ready to shiprecord receiptreceivedWarehouse event must match item.
receivedrecord settlementrefundedPayments owns settled-money truth.

The state machine answers what is legal. Data ownership answers who may establish each fact. Prefer one authoritative writer for a piece of state; other modules receive a contract, event, or read model. A shared database does not require shared ownership. If both Journey and Payments can set refunded, the invariant lives nowhere.

The principle behind the principle

State comes before transport because the transport is replaceable and the state is not. You can move from REST to gRPC to a queue in a quarter; you cannot unwrite four million rows whose meaning was never agreed. Underneath that, a state machine is a claim about which histories are possible, and every bug in this family is a history the code permitted and the model forbade, or the reverse. Enumerating transitions turns an unbounded space of field combinations into a small set you can test exhaustively—and small enumerable sets are the only things teams reliably reason about under pressure.

Single-writer ownership works for a related reason. An invariant can only be enforced where all the information needed to check it is present and serialized. Two writers means the rule has no home: each one is correct in isolation and the pair is wrong, which is why these defects are so hard to reproduce. Ownership is not a governance preference; it is where you put the check.

The boundaries: do not force a state machine on an entity with one terminal transition, or on what is genuinely a set of independent flags—an artificial machine over orthogonal facts produces a combinatorial mess that models nothing. And single-writer stops being achievable when latency or availability forbids a coordination point. Then you do not pretend; you choose convergence rules, make the conflict visible in the model, and decide who resolves it.

For every command, API, or event contract, answer:

Design retries with an idempotency key representing one intent. A client retry after timeout must return the original return and label, not create another. Design consumers as tolerant readers, and evolve persisted data with expand/contract. Compatibility is a delivery feature, not cleanup.

Draw data flows across trust boundaries and minimize data early. The carrier needs recipient name and address for a label; it does not need return reason notes or order history. Record retention and deletion in the model, not only a policy document.

How the lead actually reviewed it

Week 6, contract review, ninety minutes, with Asha from the order platform and Sam from privacy in the room. The lead's first move is not to read the API draft. She scans the state table for any row where two components could write the same fact, and finds one: refunded, which both the journey module and the payments context believed they owned. Nobody had disagreed, because nobody had asked.

Her second move is the question that has repaid itself on every initiative she has led. She asks the producer and the consumer separately: what does a timeout on create_label mean? Journey answered “no label was created.” The engineer holding the carrier adapter answered “maybe a label was created.” Both answers are reasonable and they cannot both be built on. The four-hour spike settled it: retrying a timed-out label call without an idempotency key produced a duplicate label in 12% of attempts.

The obvious fix—a unique database constraint on the return—was rejected, and the reason matters. It converts a duplicate into an error at the wrong layer, at the wrong time, and it throws away the thing the caller actually needs: the label that was already bought. The design chosen instead stores the intent before the call and returns the original answer to every retry:

class DuplicateIntent(Exception):
    """Same intent key, different content — a client bug, not a retry."""


def request_label(store, carrier, intent_key, return_id, address):
    existing = store.find_label(intent_key)
    if existing is not None:
        if existing.return_id != return_id:
            raise DuplicateIntent(intent_key)
        return existing.label_id            # a retry gets the first answer

    reservation = store.reserve(intent_key, return_id)   # committed before the call
    label_id = carrier.create_label(reservation.id, address)
    store.attach_label(intent_key, label_id)
    return label_id

Rui dissented: the carrier offers its own idempotency header, so why hold state on this side? The lead recorded the objection and answered it with a boundary argument rather than a technical one—the carrier's semantics are outside the team's control and can change with a vendor release, while the promise made to the customer cannot. The header is used as well; it is not relied on.

Sam's contribution took four minutes and removed a liability. The carrier needs a recipient name and an address to print a label. It was being sent the return reason and the order history because the payload was built from an existing internal object. Minimizing that payload at design time cost nothing; removing it after two consumers depend on it would have been an expand/contract migration across a vendor boundary.

The consequence appears at the staff-account gate in lesson 0049: fifty cases, zero duplicate labels. Nothing was celebrated, which is what a well-designed contract feels like from the outside.

Field notes · contract questions syntax will not answer
QuestionBad defaultDesigned answer
Who may move this state?Anyone with database access.One command owner enforces transitions.
What did timeout mean?Assume failure and repeat.Retry one intent idempotently.
Can a field disappear?Coordinate a big-bang deploy.Expand, migrate readers, contract.
Why retain this PII?Storage is cheap.Purpose, duration, deletion owner.
What people get wrong after reading this
Idempotency means an operation can be retried safely.
It means one intent has one result. A retry that generates a fresh key is a new intent, and most duplicate-charge incidents are key-generation bugs on the client, not handler bugs on the server. Decide where the key is minted and write it into the contract.
One authoritative writer means one service owns the table.
Ownership is about who may establish the fact. A shared schema with a single writing module is fine; two services writing the same rows from separate deployments is not, whatever the diagram says.
Backwards compatibility is cleanup for the end.
It is what allows the two sides of a change to deploy on different days—which is what allows you to deploy at all without a coordination meeting. Treat it as a delivery feature and the release calendar stops being a constraint on design.
Privacy decisions belong to legal.
The design decides what is collected and where it crosses a boundary; legal can only approve or forbid what the design already made possible. By the time a review happens, the cheap moment—choosing not to send the field—has passed.

When this is the wrong move

A full state-and-contract review is waste for a read-only internal report, or for an entity that is created and never changes again. Inside a single deployable, where one module calls another and both ship together, a typed function signature is the contract; adding a versioning policy and a compatibility window to it buys nothing and costs a document nobody updates. The apparatus earns its cost exactly where the two sides can be deployed, operated, or changed independently—which is also the moment most teams notice it is missing.

Skill: locate the durable promise

Who should set “refund settled”?

An idempotency key should identify:

A consumer receives a field it does not recognize. It should:

Practice: review one critical state transition

Draw the state machine for one initiative entity. For every transition, name command, actor, rule, owner, emitted fact, retry behavior, and audit need. Then choose one cross-boundary contract and document its meaning, failure semantics, compatibility policy, PII, and a contract test.

In the lab and the capstone. The lab's phase-4 checkpoint makes the retry semantics executable: a test that replays one intent twice and fails if the system produces two labels. The worked artifact is the state and contract review in the strong capstone packet, which names one writer per fact and states what a timeout means before any endpoint is designed. Compare with the weak version, where “status” is a free-text field, two components update it, and the failure semantics are left to whoever writes the client.

Reveal: the contract review test

Ask the producer and consumer separately what success, timeout, duplicate, missing field, new field, and deleted user mean. Any different answer is already a production defect; the code has merely not executed it yet.

Teach it back
  1. In two sentences, explain to a new lead why two writers of one fact means the invariant has nowhere to live.
  2. Take one contract you own. Ask its producer and its consumer, separately, what a timeout means—then write down whether the two answers matched.
  3. Predict the failure: if nobody names the authoritative writer for a state, what does the reconciliation script look like in a quarter, and who ends up running it by hand each Monday?

Your win

You can design a system's durable promises: legal transitions, authoritative writers, safe retries, evolvable contracts, and purposeful data lifecycles.

Read and watch deeper

Ask your agent-teacher to generate adversarial sequences—duplicate, reorder, timeout, stale read, unauthorized identifier—then decide which your contract prevents or absorbs.