Translating a Domain into Known Problems
Open in the interactive app: practice, quiz, mock interview →In one minute
- Translation is a repeatable procedure: list the domain's nouns, give each a role (resource, actor, event, process), write the invariants in the domain's own words, then turn each invariant into a technical constraint.
- Invariants are the bridge. "A driver has at most one active trip" becomes a unique constraint on active trips per driver; "money in equals money out" becomes a double-entry ledger with a reconciliation job.
- Domain experts own the invariants and the parameters. Engineers own the mapping to mechanisms. Keep both visible in the same document.
- Translate back as well: every technical failure mode must be described in domain terms ("a rider waits forever") so experts can judge its cost.
- A translated spec is also the best input for a coding agent: invariants become tests, and mechanisms become explicit instructions.
How it works
Step 1: collect the nouns and verbs
Sit with the domain expert, read the support tickets and the existing code, and write down every noun and verb people use. This is the knowledge-crunching and shared-language work of domain-driven design (Fowler on ubiquitous language; Domain-Driven Design (Evans, 2003), Ch 1, "Crunching Knowledge", and Ch 2, "Communication and the Use of Language"). In a warehouse: SKU, bin, pick list, wave, picker, cart, replenishment. In an agent platform: task, tool call, plan, sub-agent, budget, approval. Do not translate yet; just collect.
Step 2: give each noun a role
Most nouns fall into four roles. Resources are things that can be scarce or owned: bins, stock units, GPU slots, budgets. Actors act on resources: pickers, customers, agents. Events are facts that happened: "picked", "tool returned", "approved". Processes are multi-step activities: a wave, an order fulfilment, an agent's plan. Roles tell you which shapes can appear. Resources invite contention and hot spots. Events invite duplicates and ordering problems. Processes invite partial failure and multi-step consistency problems.
Step 3: write the invariants in domain words
An invariant is something that must always be true. Write it in the expert's words and get them to sign it off. "A bin's count never goes negative." "A picker works on one wave at a time." "An agent never spends more than its task budget." "A tool that sends email runs at most once per approval." Experts can tell you which invariants matter and how much a violation costs, which is the information engineers usually lack.
Step 4: turn invariants into constraints
Each invariant maps to a known mechanism, chosen by shape:
- "Never negative" or "never more than N": contention. Compare-and-set or a
CHECKconstraint with a conditional update. - "At most one X per Y": contention. A unique (often partial) index, such as
UNIQUE (picker_id) WHERE state = 'active'. - "At most once per approval": duplicates. An idempotency key derived from the approval ID.
- "Status never goes backwards": ordering. A version or sequence check on update.
- "Either all of it or none of it": multi-step consistency. A local transaction if one database, otherwise a saga with compensations and an outbox.
- "Always adds up": trust but verify. A reconciliation job comparing two independent records.
Step 5: translate failures back
For each mechanism, ask how it fails and describe that failure in domain words. A compare-and-set that loses under contention means "the picker is told the bin is busy and gets another one". A saga step that fails means "the customer is refunded within an hour". The compensation itself can fail, so it must be an idempotent command, and some steps (a sent email, a legally binding action) cannot be undone at all; the Azure pattern says to mark those points of no return and place them after the checks that can still fail (Azure: compensating transaction). Experts can then say whether that is acceptable, which turns arguments about technology into decisions about the business.
Down the stack
Layer 1The contract
Translation produces a document with four tables: glossary (noun, role), invariants (in domain words, owner, cost of violation), mechanisms (invariant, shape, mechanism, test), and failure translations (mechanism failure, domain effect, accepted or not). A reviewer can check that every invariant has a mechanism and a test, and every mechanism has a domain-language failure.
Layer 2The mechanism
The mappings are concrete. UPDATE bins SET qty = qty - $1 WHERE id = $2 AND qty >= $1 enforces "never negative" in one statement; zero rows updated means the pick must go elsewhere. In Postgres, CREATE UNIQUE INDEX one_active_wave ON assignments (picker_id) WHERE state = 'active' enforces "one wave at a time"; a second insert fails with error code 23505 (unique_violation; Postgres error codes). An idempotency table with PRIMARY KEY (approval_id) and INSERT ... ON CONFLICT DO NOTHING enforces "at most once per approval"; AWS also stores the original parameters with the token, so a retry with changed parameters is rejected instead of silently replayed (AWS Builders' Library). Each mechanism converts a sentence into something the database refuses to violate, which is stronger than application code that checks first and writes second.
Layer 3The machine
Pushing invariants into the database works because the database serialises the conflicting part on the machine. A conditional UPDATE takes a row lock. Postgres records row locks in the row itself rather than in a shared-memory table, so locking a row can cause a disk write but there is no limit on how many rows one transaction can lock (Postgres explicit locking). Checking and writing therefore happen while no other writer can touch the row. A unique index check happens inside the B-tree insert: the inserting backend locks the leaf page, finds any equal key, and waits if another transaction inserted the same key and has not committed yet. These are microsecond operations on one machine, against millisecond round trips if your application tries to coordinate the same thing across services.
Why it's built this way
- Domain experts know which rules matter but not how systems fail; engineers know the reverse.
- Invariants written in domain words are something both sides can read and agree on.
- Each invariant falls into a known shape, and each shape has known mechanisms.
- The strongest mechanisms put the invariant where conflicting writes are serialised anyway, usually the database.
- Translating failures back into domain words lets the people who own the cost decide what is acceptable.
Diagram
flowchart LR N["Nouns and verbs"] --> R["Roles: resource, actor, event, process"] R --> I["Invariants in domain words"] I --> S["Shape per invariant"] S --> M["Mechanism and test"] M --> F["Failure in domain words"] F --> X["Expert decides: accept or redesign"]
Worked example
An AI platform team is asked to let agents call paid external tools: web search at 0.5 cents a call, a code sandbox at 2 cents a minute, and an email tool. Nouns: task, agent, sub-agent, tool call, budget, approval. Roles: budget is a resource, agents are actors, tool calls are events, the task is a process.
Invariants from the product owner: a task never spends more than its budget (default 5 dollars); an email is sent at most once per human approval; a task's final answer never reflects a tool result from a cancelled sub-agent.
Translation: the budget is contention, because 20 sub-agents may spend in parallel, so each spend is a conditional decrement on the task's budget row (UPDATE budgets SET remaining = remaining - $1 WHERE task_id = $2 AND remaining >= $1). The email rule is duplicates: the email tool takes the approval ID as its idempotency key. The cancelled sub-agent rule is ordering: each sub-agent result carries the plan version it was produced under, and results from an older version are dropped.
Failure translations: under heavy contention a sub-agent's spend can be refused, so "the agent reports it ran out of budget and asks the user". Accepted. A crash between sending the email and recording it would mean "the email may be sent but marked unsent", so the team sends via a provider that also honours idempotency keys. At 2 million tool calls a day, about 23 a second, the budget row for one task sees at most 20 concurrent writers: trivial for a row lock.
In the wild
- Knight Capital, 2012: an invariant nobody wrote down. A repurposed flag meant the eighth of eight servers ran retired trading logic that no longer tracked filled quantity. In about 45 minutes it made over 4 million executions in 154 stocks and lost more than $460 million. The SEC cited the missing second-person review of the deploy and the absence of written procedures (SEC order). "Total filled never exceeds the parent order" is exactly the kind of domain invariant this lesson says to state and enforce.
- Twilio, 2013: money in versus money out. A read-only Redis master left balances at zero, and auto-recharge ran repeatedly for 1.4% of customers. Twilio's fixes were fail-safes that block charges when balances cannot be written, and validation against a double-entry ledger (Twilio post-mortem). That is the reconciliation row in the invariants table above.
- Stripe, 2017: online migration with side-by-side checks. Stripe moved subscriptions out of customer records without downtime by dual-writing, backfilling, switching reads, then writes, then deleting the old path, running old and new read paths side by side with GitHub's Scientist library to flag differences (Stripe engineering). Each step carried a checkable invariant.
Trade-offs
| Where the invariant lives | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Database constraint | Cannot be bypassed; atomic | Limited to one database; harder to change | The rule is critical and data is in one place |
| Application check then write | Flexible, easy to express | Races between check and write | Low-stakes rules, or behind a lock |
| Single-writer service per entity | Serialises any logic, across stores | Throughput per entity is limited | Complex rules on one entity (an account, a task) |
| Reconciliation after the fact | Works across systems and vendors | Detects late, needs repair | Cross-company flows such as payments |
Pitfalls in production
- Translating the nouns but not the invariants, which yields a nice data model that allows bad states. Fix: list invariants before tables.
- Invariants written only in technical words, so experts cannot review them. Fix: keep the domain sentence next to the constraint.
- Check-then-write in application code for a critical rule. Fix: a conditional write or a constraint.
- Forgetting that agents are actors too: they retry, run in parallel and act on stale plans. Fix: give agent actions the same idempotency and version checks as human ones.
- Parameters hard-coded by engineers (budget, hold time). Fix: make them configuration owned by the domain team.
Leadership lens
- Make the four tables (glossary, invariants, mechanisms, failure translations) a required part of design docs for new domains.
- Invite the domain expert to review the invariants and failure translations, not the architecture diagram.
- Each invariant should have a test, and the critical ones a production check that alerts when violated.
- Use the translated spec as the agent prompt: invariants as tests to write first, mechanisms as instructions.
Check yourself
Translate "a hotel room is never assigned to two guests on the same night" into a mechanism.
Show answer
Contention on a room-night resource: a unique constraint on (room_id, night) in the assignments table, or a conditional update on a room-night row, so a second assignment fails atomically.
Why translate failure modes back into domain language?
Show answer
Only domain owners know the cost of a failure, such as how bad it is for a rider to wait or a payment to be delayed, so they need it described in their terms to decide what is acceptable.
An agent platform must ensure a task never exceeds its budget while 20 sub-agents run in parallel. Which shape, and why is "read the remaining budget, then spend" wrong?
Show answer
Contention. Two sub-agents can both read 1 dollar remaining and both spend 80 cents; the check and the write must be one atomic conditional update.
When is reconciliation a better home for an invariant than a database constraint?
Show answer
When the data that must agree lives in different systems or companies, such as your ledger and a bank's, so no single transaction can enforce it.
Sources and further study
Primary
- Pat Helland, "Life beyond Distributed Transactions: An Apostate's Opinion" (ACM Queue): when atomicity is limited to one entity, cross-entity rules must be handled with messages and tolerance for repeats.
Systems docs
- PostgreSQL error codes and explicit locking: how constraints and row locks enforce invariants.
- AWS Builders' Library: making retries safe with idempotent APIs.
- Azure: compensating transaction pattern.
Books
- Domain-Driven Design (Evans, 1st ed., 2003), Ch 1, "Crunching Knowledge", Ch 2, "Communication and the Use of Language", and Ch 5, "A Model Expressed in Software".
- Designing Data-Intensive Applications (1st ed.), Ch 7, "Transactions" (constraints, atomic compare-and-set), by Martin Kleppmann.
Other curricula
- Martin Fowler: ubiquitous language: why the invariants table should use the expert's own words.
- System Design Primer: the database and idempotency sections for the mechanisms this lesson maps invariants onto.
Free resources
- Domain-Driven Design reference (Eric Evans, PDF): the canonical summary of ubiquitous language and bounded contexts.
- Ubiquitous Language (Martin Fowler): why engineers and experts should share words.
- PostgreSQL: constraints: how CHECK, UNIQUE and exclusion constraints enforce invariants.
- Azure: compensating transaction pattern: undoing multi-step work when no single transaction exists.
- Making retries safe with idempotent APIs (AWS Builders' Library): turning "at most once" rules into mechanisms.