X-driven: the map
Article.
Every few years another “-driven” approach arrives, and interviewers use the vocabulary as a probe: do you know what each one actually optimises for, and can you say which ones are live practice versus history. This is the map; the ones that carry interview weight have their own notes, linked.
The organising question is what comes first — what you model, or what you write. The second question, the one that separates a senior answer: is it a way of designing the software, or a way of working? DDD, event-driven and type-driven shape the code itself; TDD, BDD and ATDD shape the order in which you write it and leave no trace in the architecture. Confusing the two (“we do TDD, so our design is clean”) is the common mistake.
“Which of these do you actually use, and which would you not?” is the question to be ready for; the end of this note answers it.
Driven by what you model
| Approach | The thing that shapes the code |
|---|---|
| Domain-driven | the business language |
| Database-first | the schema |
| Event-driven | the events |
| Type-driven | the types |
| Responsibility-driven | who does what |
| Attribute-driven | quality requirements |
| Risk-driven | what could go wrong |
Domain-driven design puts the business domain and its vocabulary at the centre — bounded contexts, aggregates, a ubiquitous language shared with the people who do the work. The live one, and the one interviews probe hardest. Domain-Driven Design (DDD).
Schema, events and types
Database-first derives everything from the schema. Django’s default
posture: models are tables, and the ORM is the application’s shape. Fast to
start, and the reason a Django app’s domain logic so often ends up in
models.py — see
Django Models vs QuerySets: Understanding the Difference.
Event-driven makes events the primary building block, which leads naturally to event sourcing and CQRS. Event-Driven Architecture and CQRS and Event Sourcing.
Type-driven encodes the rules in types first so that illegal states cannot be constructed. Haskell and Rust culture, and now practical in Python with strict typing plus Pydantic. Type-driven development: make illegal states unrepresentable.
Responsibilities, qualities and risk
Responsibility-driven design (Wirfs-Brock) assigns responsibilities to objects before deciding their data; CRC cards — class, responsibilities, collaborators, on index cards — come from here. GRASP is its modern descendant: GRASP — General Responsibility Assignment Software Patterns.
Attribute-driven design starts from quality attributes — latency, availability, auditability — and lets them decide the architecture. It is what a good system-design answer does implicitly when it asks “what is the p99 requirement” before drawing boxes. System Design Interview — The Framework.
Risk-driven architecture (Fairbanks) says do exactly as much design as your actual risks justify and no more: identify what could fail expensively, design against that, ship the rest. The most useful of the lesser-known ones, because it answers “how much architecture?” — which nobody else does.
Data-oriented design is unrelated despite the name: memory layout and cache behaviour drive structure — arrays of structs versus structs of arrays. Game engines and numeric kernels. In Python the same instinct is why NumPy and Polars exist; see Why Python is slow — and how to mitigate it.
Model-driven engineering (MDE/MDA) generates code from formal UML models. Enterprise legacy; know the name, do not propose it.
Use-case-driven (Jacobson, ICONIX) lets use cases drive the object model. Absorbed into agile user stories; the name rarely comes up now.
Driven by what you write first
| Approach | First artifact |
|---|---|
| TDD | a failing test |
| BDD | a business-readable scenario |
| ATDD | an acceptance example agreed with stakeholders |
| Spec-driven | a precise spec |
| Contract-first | the API contract |
| Design by contract | pre- and postconditions |
| README-driven | the docs |
TDD — tests before implementation, in a short loop: write a failing test (red), the least code that passes (green), then improve the code with the test as a safety net (refactor). Test Strategy and the Test Pyramid.
TDD and BDD in one run
A business-readable scenario, executed by pytest-bdd 9.0 on pytest 9.0:
Feature: Loyalty discount
Scenario: Gold customers get 10% off orders over 100
Given a gold customer
When they order goods worth 150
Then the total is 135@when(parsers.parse("they order goods worth {amount:d}"), target_fixture="result")
def order(tier, amount):
return total(amount, tier)
@then(parsers.parse("the total is {expected:d}"))
def check(result, expected):
assert result == expected
def test_no_discount_under_100(): # the unit-level TDD loop
assert total(80, "gold") == 80red: FAILED test_gold_discount - assert 150 == 135 (1 failed, 1 passed)
green: 2 passed in 0.01s (after implementing)The scenario is the outer loop, a statement of behaviour someone outside engineering can read and agree to; the unit tests are the inner loop. BDD is TDD whose first test is written in the business’s language.
The rest of the write-first family
BDD and ATDD / Specification by Example — TDD with Given/When/Then scenarios, and acceptance criteria as executable examples agreed with the business. BDD, ATDD and Specification by Example.
Spec-driven — a precise spec, then implement against it. The recent wave (GitHub Spec Kit, AWS Kiro) hands the spec to an agent, but the idea predates AI tooling. Spec-driven development.
Contract-first / API-first — OpenAPI, protobuf or a GraphQL SDL before any implementation. FastAPI is code-first by design, which creates a tension worth being able to explain. Contract-first API design.
Its sibling consumer-driven contracts (Pact) makes the consumer’s expectations the contract: Integration and Contract Testing.
Design by contract (Meyer) — preconditions, postconditions and invariants
as first-class code, checked at runtime. icontract in Python:
import icontract
@icontract.require(lambda amount: amount > 0)
@icontract.ensure(lambda result, amount: result.balance >= 0)
def withdraw(account: Account, amount: int) -> Account: ...Rarely used as a library in Python, but the idea is everywhere: Pydantic
validators are preconditions on construction, and an assert at the top of a
function is a precondition nobody named. The interview answer is that
type-driven design moved most contracts into the type checker.
README-driven — write the README first as a forcing function on the design. If the usage example is awkward to write, the API is awkward. Cheap, and it works.
The formal end
TLA+, Alloy, Z, B-method — mathematical specification plus model checking, used where mistakes are very expensive. The interview anecdote is AWS using TLA+ on DynamoDB and S3’s distributed protocols and finding bugs that testing had not, because a model checker explores every interleaving where a test explores one. Know the story; nobody expects you to have written TLA+.
Domain-driven design in one example
The one that interviews probe hardest. Its core is not the patterns but boundaries and language: each bounded context has its own model and words, and an aggregate is the unit that keeps an invariant true inside one transaction.
class Order: # aggregate root
def __init__(self, order_id: OrderId, limit: Money):
self._lines: list[Line] = []
self._limit = limit
def add_line(self, sku: Sku, qty: int, price: Money) -> None:
if self.total() + price * qty > self._limit:
raise CreditLimitExceeded(self.total(), self._limit) # the invariant
self._lines.append(Line(sku, qty, price))All changes to lines go through the root, so the credit limit cannot be bypassed; other aggregates are referenced by id, and consistency between aggregates is eventual, through domain events (Domain-Driven Design (DDD)).
The architectures that ride along
Hexagonal (ports and adapters), Clean and Onion are one idea — domain in the centre, IO at the edges — and they usually arrive alongside DDD. Hexagonal Architecture (Ports and Adapters), Clean Architecture.
Vertical slice architecture is the current counterweight — organise by feature, not by layer: Vertical slice architecture.
The combination that shows up in real systems: DDD for boundaries, hexagonal for structure, TDD or BDD for the inner loop, and contract-first at the seams between services. Being able to say that sentence, and why each piece is where it is, is worth more than knowing any one of them deeply.
Where it breaks in production
TDD that tests the mocks
Every collaborator is mocked, each test asserts which methods were called, and refactoring breaks dozens of tests while no behaviour changed. The tests pin the implementation, not the behaviour. Test through public behaviour, fake only the slow or external edges (a fake repository, not a mock of every call), and let the tests survive refactors.
Gherkin nobody outside engineering reads
BDD scenarios written by developers for developers, with step definitions as a second layer to maintain. They cost more than plain pytest and buy nothing, because the point of BDD, a shared executable agreement with the business, never happened. Use it where product or domain experts actually review the scenarios; elsewhere, well-named tests are enough.
DDD ceremony on a CRUD app
Aggregates, repositories, domain events and four layers around what is essentially forms over a table. The patterns cost indirection everywhere and protect no real invariant. The strategic half (bounded contexts, shared language) is cheap and almost always useful; the tactical patterns are for the parts of the domain with real rules.
Event-driven without the tooling
Events everywhere, and nobody can answer “what happened to order 42?” without reading six consumers’ logs. Event-driven systems need correlation ids, tracing across the broker, and a way to replay; without them the coupling is still there, only invisible (Event-Driven Architecture).
The trade not taken
The alternative to choosing a named approach is having none: writing code and tests in whatever order, designing as needed. For small, short-lived code that is often right, and it is what risk-driven design legitimises: spend design effort where failure is expensive.
The named approaches are worth adopting deliberately, one at a time, for a reason: DDD when the domain is the hard part, TDD where correctness matters and the design is still moving, BDD when the business will read the scenarios, contract-first where teams meet.
Related
Interview angle 6
- “Which of these do you actually use?” - name the combination and place each: DDD to find the boundaries, hexagonal to keep IO at the edges, TDD in the inner loop, contract-first between services. Then say which you would not use and why - MDA, formal methods for a CRUD app.
- “TDD or BDD?” - the same loop at different altitudes. TDD is the
developer’s inner loop; BDD adds a scenario the business can read, and it
pays for itself only when the business actually reads it.
- Follow-up: “What is ATDD?” - BDD’s acceptance-level cousin, where the examples are agreed with stakeholders before the work starts.
- Follow-up: “Your TDD suite breaks on every refactor. Why?” - it tests the mocks: assertions on which methods were called pin the implementation. Test behaviour, fake only the external edges.
- “Which of these are process and which are design?” - TDD, BDD and ATDD are ways of working: they decide what you write first and leave no trace in the architecture. DDD, event-driven and type-driven shape the code itself.
- “When is DDD overkill?” - when the domain has no real rules: forms over tables. Keep the strategic part (bounded contexts, shared language), skip the tactical patterns.
- “How much architecture is enough?” - risk-driven. Identify the failures that would be expensive, design against those, and ship the rest with the simplest thing. The failure mode in both directions has a name: big design up front, and no design at all.
- “Where would formal methods be worth it?” - a distributed protocol where a rare interleaving loses data. AWS’s TLA+ work on DynamoDB is the example; a model checker explores every interleaving, a test explores one.