skip to content

Outside-In vs Inside-Out

The two TDD schools: starting at the outer boundary and discovering collaborators as you go, versus growing from a domain core with real objects. Asked the moment a candidate claims TDD.

on this pageshow

questions

3

In TDD, what is the difference between outside-in and inside-out development?

level: juniorimportance: must knowfreq 68%

answer

  1. Same cycle, different starting point
  2. Start at the edge or at the core
  3. Collaborators invented versus already built
  4. London or mockist versus Detroit or classicist

basics

~20 s

Outside-in TDD starts at an outer boundary and invents collaborator roles as they are needed, standing them in until they are built. Inside-out TDD starts with domain behaviour built from real objects and wires the outer layers in last.

solid answer

~50 s

Both schools follow the same red-green-refactor cycle; they disagree about where to start. **Outside-in** (the London or mockist school) writes the first test at an outer boundary — the entry point a request, job or message arrives at. The object under test needs collaborators that do not exist yet, so you invent each role, fix its signature, and put a stand-in in its place; that role becomes the next test's subject, and you recurse inward. **Inside-out** (the Detroit or classicist school) writes the first test on a small piece of domain behaviour using real objects, then composes outward, connecting to the outside world last. The consequence is the trade-off: outside-in leaves more stand-ins and more assertions about how collaborators were called, while inside-out asserts mostly on values and state but discovers the outer design late.

code

pseudocode · 9 lines
pseudocode
test "ingest accepts a valid meter reading":
    validator = standIn(ReadingValidator)
    store     = standIn(ReadingStore)
    program validator.check(reading) to return ACCEPTED

    handler = IngestHandler(validator, store)
    handler.ingest(reading)

    verify store.save(reading) was called once

go deeper

for a junior

Be ready to state both starting points in one sentence each and to give the alternative names — London or mockist for outside-in, Detroit or classicist for inside-out. Knowing that both use the same failing-test-first cycle is half the answer.

for a middle

An interviewer expects the mechanics: why an outside-in test cannot use a real collaborator yet, why that pushes assertions towards how calls were made, and why an inside-out test can usually assert on a returned value or on state instead.

for a senior

Show that you pick per feature rather than by habit, and that you can name the failure mode each style leaves behind: a green suite over a mismatched seam, versus an awkward boundary discovered after the core has set.

for a principal

Own the position that this is a trade-off, not a doctrine, and that the evidence for either school is contested. Be able to say what you would standardise across teams — probably the seam-verification rule, not the starting point.

### Where the two schools actually differ Test-driven development tells you to write a failing test, write just enough code to pass it, then restructure. It does not tell you **where in the system to start**, and that silence is the whole of the disagreement. Two traditions grew up around the answer: **outside-in**, also called the London or mockist school, and **inside-out**, also called the Detroit or classicist school. ### Outside-in: invent the collaborator you wish you had Outside-in begins at an outer boundary of the change — the entry point a user action, a scheduled job or an inbound message arrives at — and writes a test for the behaviour that is visible there. Almost immediately the object under test needs collaborators that do not exist yet. Instead of stopping to build them, you **invent the role**: you decide what you wish you could call, name it, fix its signature, and put a stand-in in its place so the test can run and fail for the right reason. The stand-in is a captured design decision, not merely a shortcut — you have just specified an interface from the caller's point of view, at the exact moment the caller needed it. When the outer test passes, each invented role becomes the subject of the next test and you recurse inward. The design therefore arrives as a chain of small interfaces, each shaped by its one known caller, and the outermost behaviour is demonstrably reachable from the very first cycle. ### Inside-out: grow real objects and wire them late Inside-out begins at the core of the problem: a small piece of domain behaviour that can be tested through its own public surface with no scaffolding. You build that, then the piece that composes it, then the piece that composes *those*, and the connection to the outside world is made last. Collaborators are usually the real objects, because by the time an outer object needs one it has already been built and tested. Stand-ins appear only where the real thing is genuinely unusable in a test — a network hop, a clock, a device, a source of randomness. ### What the tests end up asserting, and why that is the trade-off An outside-in test often has to say something about **how** the object under test used its collaborators, because at the moment it is written there is no real collaborator whose state you could inspect afterwards. An inside-out test can usually assert on a returned value or on the state of real objects. Everything else follows from that. Outside-in couples tests to call structure: a behaviour-preserving change that moves a call to a different collaborator, merges two roles, or reorders an interaction can turn the suite red even though nothing observable changed. It also leaves many stand-ins in place permanently, and a stand-in only ever agrees with the contract you imagined, not the one the real collaborator honours. Inside-out pays elsewhere. Failures localise poorly — a defect in a core object can redden dozens of tests at every layer above it, and setup grows as real object graphs get deeper. The outer boundary is discovered last, so an awkward entry point or a missing seam is found after the core has already committed to a shape, which is the more expensive moment to learn it. ### A worked example A team of eleven is building an ingest path for a smart-meter reading feed: accept a reading, validate it, discard duplicates inside a rolling window, convert raw pulse counts to kilowatt-hours, store the result. Driven outside-in, the first test exercises the ingest handler and invents two roles on the spot — something that judges a reading acceptable and something that stores it — standing both in. Two cycles later those roles are real tests of their own. The seams exist from day one, which is convenient when several pairs are working at once. Driven inside-out, the first test is on the pulse-to-kilowatt-hour conversion with a real converter and a real reading, and the handler is written last, over objects that already work. Nothing is stood in until the storage boundary is reached. ### Which one is right Neither, as a blanket rule. The claim that one school reliably produces better designs is contested; the published evidence is thin and mostly experiential, so an interviewer asking this wants your reasoning, not a verdict. Most experienced practitioners mix: outside-in to discover seams and to keep an end-to-end behaviour honest, inside-out for the parts where the risk is a gnarly rule rather than an unclear collaboration, and a deliberate pass to replace stand-ins with the real collaborators once those exist.

  • Why does the outside-in style tend to produce narrow, single-purpose interfaces?
    Because each role is invented by exactly one caller at the moment that caller needs it. You write down only the operation you wish you could call, so the interface starts with one method shaped by a real use, rather than a guessed set of operations a future caller might want. Roles widen later only when a second caller genuinely needs more.
  • Do the two schools disagree about the red-green-refactor cycle itself?
    No. Both write a failing test first, add just enough code to pass, then restructure with the suite green. The disagreement is only about where in the system the first test is aimed, and therefore how many collaborators must be stood in to make that test runnable.
  • Is one school proven to produce better designs than the other?
    No — that claim is contested. The published evidence is thin and largely experiential, and both schools have produced well-regarded systems. The defensible position is that they surface different information first: outside-in surfaces collaboration and boundary design early, inside-out surfaces domain rules early. Experienced teams usually mix them per feature.

Outside-in is ordering from the menu and letting the kitchen work out what it must buy; inside-out is cooking with what is already in the pantry and writing the menu once you know what you can make.

saying these in an interview costs you the question

  • Says inside-out means never using any test double
  • Says outside-in means writing end-to-end tests only
  • Treats the two schools as different cycles rather than different starting points
  • Claims one school is objectively proven to produce better designs
  • Cannot say why outside-in leaves stand-ins behind

context

open as a page

Why does outside-in TDD leave more test doubles behind than inside-out TDD?

level: middleimportance: should knowfreq 54%

basics

~20 s

Outside-in starts before the collaborators exist, so each invented role needs a stand-in for the test to run — and nothing forces a later swap for the real object. Inside-out builds collaborators first and uses them directly.

open as a page

When starting a feature with TDD, how do you choose outside-in or inside-out?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Start where the uncertainty is. If the collaboration and the boundary are unclear, drive outside-in and invent roles at the moment of need; if the boundary is agreed and a domain rule is the hard part, drive inside-out.

open as a page