Methodologies / 11_driven_by_what.md

X-driven: the map

Updated 6 interview angles 9 min read source
On this page9
  1. Driven by what you model
  2. Driven by what you write first
  3. The formal end
  4. Domain-driven design in one example
  5. The architectures that ride along
  6. Where it breaks in production
  7. The trade not taken
  8. Related
  9. Interview angle

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:

gherkin
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
python
@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") == 80
text
red:    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:

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.

python
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.

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.