{"course":{"slug":"how-software-changed","title":"How Software Development Changed","url":"https://addaly.com/learn/how-software-changed","level":"intermediate","track":"work"},"lesson":{"slug":"interfaces-before-implementations","title":"Design the seam, generate the filling","url":"https://addaly.com/learn/how-software-changed/interfaces-before-implementations","position":35,"of":87,"minutes":9,"answer":"Interfaces are commitments and implementations are disposable, so spend your thinking on the signature, the types and the documented contract.","keyIdea":"Interfaces are commitments and implementations are disposable, so spend your thinking on the signature, the types and the documented contract, then let the body be generated against tests that came from the contract rather than from the code."},"body":{"format":"markdown","text":"## The asymmetry that should govern your effort\n\nAn implementation can be replaced in an afternoon. An interface, once other code depends on it, is a commitment: every caller has to change, in lockstep, and if the callers are outside your repository you may never be able to change it at all.\n\nThat asymmetry always existed. It got sharper when implementations became cheap, because the ratio between the two costs widened. Your attention should follow the ratio.\n\n> Figure: Where the thinking should go. A two by two grid, rows The interface and The implementation, columns Cost to write and Cost to change later: The interface and Cost to write: Four minutes; The interface and Cost to change later: Every caller, in lockstep; The implementation and Cost to write: Close to nothing; The implementation and Cost to change later: An afternoon. Implementations became cheap and interfaces did not move at all, so the ratio between the two costs widened. Your attention should follow the ratio.\n\nThe practical form: **spend your thinking on the signature, the types and the contract; let the body be generated and verified.**\n\n## Write the seam first, in full\n\n```python\nclass RateProvider(Protocol):\n    def rate(self, base: CurrencyCode, quote: CurrencyCode,\n             at: datetime) -> Decimal:\n        \"\"\"Exchange rate from base to quote at the given instant.\n\n        `at` must be timezone-aware. Returns the rate published most\n        recently at or before `at`, which for weekends means Friday's.\n\n        Raises RateUnavailable if no rate exists at or before `at`,\n        which happens for dates before 2019-01-01 and for currency\n        pairs we do not carry. Never returns a stale value silently.\n\n        Must be safe to call concurrently.\n        \"\"\"\n```\n\nNothing here is implementation. Every line is a decision:\n\n- The direction of the conversion is in the parameter names, so it cannot be got backwards silently.\n- `at` is required, which means somebody had to think about which instant, rather than defaulting to now.\n- Timezone-aware is stated, so a naive datetime is a documented error rather than a silent offset bug.\n- The weekend rule is stated, which is a real business decision that would otherwise be discovered in an audit.\n- The failure is named and typed rather than returning `None` and letting each caller invent a behaviour.\n- Thread safety is a contract, not an accident.\n\nThat docstring took four minutes and it specifies the implementation almost completely. Handing it over now is safe, because there is little left to guess.\n\n## Types are the machine-checked part of the contract\n\nEverything expressible in the type system should be, because that part of the contract is enforced rather than merely written.\n\n`rate(base: CurrencyCode, quote: CurrencyCode)` cannot be called with the arguments in the wrong order if `CurrencyCode` is a distinct type and the parameter names are used. `-> Decimal` prevents the float. A `RateUnavailable` exception type is discoverable in a way that a `None` return is not.\n\nThe docstring carries what the types cannot: the weekend rule, the staleness guarantee, the concurrency promise. Those are the parts that need a human, and they are the parts worth the four minutes.\n\n## Then the tests, then the body\n\nThe order matters and it is not ceremony.\n\n1. **Signature and contract**, as above.\n2. **Tests derived from the contract**, not from any implementation, because there is not one yet. Weekend behaviour. A date before 2019. An unknown pair. A naive datetime. Two threads.\n3. **The body**, generated, and checked against tests that already existed.\n\nThis is test-driven development, and its value went up rather than down. The reason is precise: TDD's benefit was never the tests. It was that writing a test first forces you to decide the interface and the behaviour before you have an implementation to be anchored by. That is exactly the thinking that gets skipped when a plausible body appears in ninety seconds, and it is exactly the thinking that a generated test written afterwards cannot supply.\n\n## Design the seams so the mistakes are cheap\n\nA useful habit when laying out a change: ask where you want the replaceable boundaries to be.\n\n- **Pure functions at the core**, effects at the edge. A pure function is trivially testable, trivially differentially testable, and safe to regenerate. A function that reaches the network in the middle of a calculation is none of those.\n- **One place per external system.** All calls to the payment provider behind one module. When the provider changes their API, and they will, you change one file and its tests, and everything else is untouched.\n- **Narrow interfaces.** A collaborator with three methods can be replaced, faked and reasoned about. One with twenty-two cannot, and it is the twenty-two-method interface that becomes impossible to modify.\n\nEach of these makes a region of the system safe to regenerate wholesale, which is what you want: a clear boundary, a precise contract, verified behaviour, and no need for anybody to read the body carefully at all.\n\n## The one rule to keep\n\nIf you cannot write the signature and the contract yourself, you do not yet understand the problem well enough to accept an answer to it. Getting one anyway is how you end up owning a design you did not choose."},"check":{"question":"Why does writing the tests before the implementation matter more now than it did, given that a model can generate tests for existing code in seconds?","options":[{"text":"Because generated tests have lower coverage than hand-written ones, so writing first ensures the important paths are exercised.","correct":false,"tempting":"Generated tests often achieve higher coverage, which is precisely why coverage is the wrong thing to compare on."},{"text":"Because tests written first run faster, being closer to unit level rather than derived from an existing implementation's structure.","correct":false,"tempting":"Test-first does tend to produce better-isolated tests, but speed is a side effect rather than the reason the ordering matters."},{"text":"Because a test written after the code takes its expected values from the code, so it cannot contradict it.","correct":true},{"text":"Because it is a professional discipline, and disciplines that were valuable when writing was slow remain valuable when it is fast.","correct":false,"tempting":"Appeals to continuity of practice without naming a mechanism, which is exactly the reasoning that lets a practice become a ritual."}]},"prev":{"slug":"what-the-tool-can-know","title":"What the tool can actually know about your codebase","url":"https://addaly.com/learn/how-software-changed/what-the-tool-can-know"},"next":{"slug":"scope-and-blast-radius","title":"Bounding what a change is allowed to touch","url":"https://addaly.com/learn/how-software-changed/scope-and-blast-radius"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Design the seam, generate the filling', in How Software Development Changed. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/how-software-changed/interfaces-before-implementations (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Design the seam, generate the filling","courseTitle":"How Software Development Changed","module":{"index":4,"slug":"saying-what-you-want","title":"Saying what you want"},"answer":null,"keyIdea":"Interfaces are commitments and implementations are disposable, so spend your thinking on the signature, the types and the documented contract, then let the body be generated against tests that came from the contract rather than from the code.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure","figure":{"cols":["Cost to write","Cost to change later"],"rows":["The interface","The implementation"],"type":"matrix","cells":[["Four minutes","Every caller, in lockstep"],["Close to nothing","An afternoon"]],"title":"Where the thinking should go","caption":"Implementations became cheap and interfaces did not move at all, so the ratio between the two costs widened. Your attention should follow the ratio.","cellLabels":[["the signature, the types, the documented contract","and callers outside your repository may never change at all"],["generated against tests that came from the contract","behaviour is fixed, so the old version is the oracle"]]},"alt":"Figure: Where the thinking should go. A two by two grid, rows The interface and The implementation, columns Cost to write and Cost to change later: The interface and Cost to write: Four minutes; The interface and Cost to change later: Every caller, in lockstep; The implementation and Cost to write: Close to nothing; The implementation and Cost to change later: An afternoon. Implementations became cheap and interfaces did not move at all, so the ratio between the two costs widened. Your attention should follow the ratio."}},"blocks":[{"t":"h2","id":"the-asymmetry-that-should-govern-your-effort","runs":[{"text":"The asymmetry that should govern your effort"}]},{"t":"p","runs":[{"text":"An implementation can be replaced in an afternoon. An interface, once other code depends on it, is a commitment: every caller has to change, in lockstep, and if the callers are outside your repository you may never be able to change it at all."}]},{"t":"p","runs":[{"text":"That asymmetry always existed. It got sharper when implementations became cheap, because the ratio between the two costs widened. Your attention should follow the ratio."}]},{"t":"p","runs":[{"text":"The practical form: "},{"text":"spend your thinking on the signature, the types and the contract; let the body be generated and verified.","b":true}]},{"t":"h2","id":"write-the-seam-first-in-full","runs":[{"text":"Write the seam first, in full"}]},{"t":"code","lang":"python","label":"Python","text":"class RateProvider(Protocol):\n    def rate(self, base: CurrencyCode, quote: CurrencyCode,\n             at: datetime) -> Decimal:\n        \"\"\"Exchange rate from base to quote at the given instant.\n\n        `at` must be timezone-aware. Returns the rate published most\n        recently at or before `at`, which for weekends means Friday's.\n\n        Raises RateUnavailable if no rate exists at or before `at`,\n        which happens for dates before 2019-01-01 and for currency\n        pairs we do not carry. Never returns a stale value silently.\n\n        Must be safe to call concurrently.\n        \"\"\""},{"t":"p","runs":[{"text":"Nothing here is implementation. Every line is a decision:"}]},{"t":"ul","items":[[{"text":"The direction of the conversion is in the parameter names, so it cannot be got backwards silently."}],[{"text":"at","code":true},{"text":" is required, which means somebody had to think about which instant, rather than defaulting to now."}],[{"text":"Timezone-aware is stated, so a naive datetime is a documented error rather than a silent offset bug."}],[{"text":"The weekend rule is stated, which is a real business decision that would otherwise be discovered in an audit."}],[{"text":"The failure is named and typed rather than returning "},{"text":"None","code":true},{"text":" and letting each caller invent a behaviour."}],[{"text":"Thread safety is a contract, not an accident."}]]},{"t":"p","runs":[{"text":"That docstring took four minutes and it specifies the implementation almost completely. Handing it over now is safe, because there is little left to guess."}]},{"t":"h2","id":"types-are-the-machine-checked-part-of-the-contract","runs":[{"text":"Types are the machine-checked part of the contract"}]},{"t":"p","runs":[{"text":"Everything expressible in the type system should be, because that part of the contract is enforced rather than merely written."}]},{"t":"p","runs":[{"text":"rate(base: CurrencyCode, quote: CurrencyCode)","code":true},{"text":" cannot be called with the arguments in the wrong order if "},{"text":"CurrencyCode","code":true},{"text":" is a distinct type and the parameter names are used. "},{"text":"-> Decimal","code":true},{"text":" prevents the float. A "},{"text":"RateUnavailable","code":true},{"text":" exception type is discoverable in a way that a "},{"text":"None","code":true},{"text":" return is not."}]},{"t":"p","runs":[{"text":"The docstring carries what the types cannot: the weekend rule, the staleness guarantee, the concurrency promise. Those are the parts that need a human, and they are the parts worth the four minutes."}]},{"t":"h2","id":"then-the-tests-then-the-body","runs":[{"text":"Then the tests, then the body"}]},{"t":"p","runs":[{"text":"The order matters and it is not ceremony."}]},{"t":"ol","start":1,"items":[[{"text":"Signature and contract","b":true},{"text":", as above."}],[{"text":"Tests derived from the contract","b":true},{"text":", not from any implementation, because there is not one yet. Weekend behaviour. A date before 2019. An unknown pair. A naive datetime. Two threads."}],[{"text":"The body","b":true},{"text":", generated, and checked against tests that already existed."}]]},{"t":"p","runs":[{"text":"This is test-driven development, and its value went up rather than down. The reason is precise: TDD’s benefit was never the tests. It was that writing a test first forces you to decide the interface and the behaviour before you have an implementation to be anchored by. That is exactly the thinking that gets skipped when a plausible body appears in ninety seconds, and it is exactly the thinking that a generated test written afterwards cannot supply."}]},{"t":"h2","id":"design-the-seams-so-the-mistakes-are-cheap","runs":[{"text":"Design the seams so the mistakes are cheap"}]},{"t":"p","runs":[{"text":"A useful habit when laying out a change: ask where you want the replaceable boundaries to be."}]},{"t":"ul","items":[[{"text":"Pure functions at the core","b":true},{"text":", effects at the edge. A pure function is trivially testable, trivially differentially testable, and safe to regenerate. A function that reaches the network in the middle of a calculation is none of those."}],[{"text":"One place per external system.","b":true},{"text":" All calls to the payment provider behind one module. When the provider changes their API, and they will, you change one file and its tests, and everything else is untouched."}],[{"text":"Narrow interfaces.","b":true},{"text":" A collaborator with three methods can be replaced, faked and reasoned about. One with twenty-two cannot, and it is the twenty-two-method interface that becomes impossible to modify."}]]},{"t":"p","runs":[{"text":"Each of these makes a region of the system safe to regenerate wholesale, which is what you want: a clear boundary, a precise contract, verified behaviour, and no need for anybody to read the body carefully at all."}]},{"t":"h2","id":"the-one-rule-to-keep","runs":[{"text":"The one rule to keep"}]},{"t":"p","runs":[{"text":"If you cannot write the signature and the contract yourself, you do not yet understand the problem well enough to accept an answer to it. Getting one anyway is how you end up owning a design you did not choose."}]}],"check":{"question":[{"text":"Why does writing the tests before the implementation matter more now than it did, given that a model can generate tests for existing code in seconds?"}],"options":[{"text":[{"text":"Because generated tests have lower coverage than hand-written ones, so writing first ensures the important paths are exercised."}],"correct":false,"tempting":[{"text":"Generated tests often achieve higher coverage, which is precisely why coverage is the wrong thing to compare on."}]},{"text":[{"text":"Because tests written first run faster, being closer to unit level rather than derived from an existing implementation’s structure."}],"correct":false,"tempting":[{"text":"Test-first does tend to produce better-isolated tests, but speed is a side effect rather than the reason the ordering matters."}]},{"text":[{"text":"Because a test written after the code takes its expected values from the code, so it cannot contradict it."}],"correct":true,"tempting":null},{"text":[{"text":"Because it is a professional discipline, and disciplines that were valuable when writing was slow remain valuable when it is fast."}],"correct":false,"tempting":[{"text":"Appeals to continuity of practice without naming a mechanism, which is exactly the reasoning that lets a practice become a ritual."}]}],"order":[3,2,1,0],"why":null},"prev":{"slug":"what-the-tool-can-know","title":"What the tool can actually know about your codebase","minutes":9},"next":{"slug":"scope-and-blast-radius","title":"Bounding what a change is allowed to touch","minutes":9}}}