{"course":{"slug":"how-software-changed","title":"How Software Development Changed","url":"https://addaly.com/learn/how-software-changed","level":"intermediate","track":"work"},"lesson":{"slug":"architectural-coherence","title":"Coherence has to be defended by something","url":"https://addaly.com/learn/how-software-changed/architectural-coherence","position":56,"of":87,"minutes":9,"answer":"No individual diff is where architectural drift gets decided, so coherence needs a written page of how this codebase does recurring things plus an automated fitness function that fails the build.","keyIdea":"No individual diff is where architectural drift gets decided, so coherence needs a written page of how this codebase does recurring things plus an automated fitness function that fails the build, while the genuinely architectural bets stay with people."},"body":{"format":"markdown","text":"## Nothing objects to a system acquiring fifteen ways to do one thing\n\nGenerated code is excellent locally and indifferent globally. Each change fits its immediate surroundings. No individual diff is the place where \"we now have three approaches to background work\" gets decided, so it never gets decided: it accumulates.\n\nThis was previously held together by two accidents. Writing was slow, so people reused rather than rewrote. And a small number of people wrote most of the code, so their preferences propagated by default. Both accidents are weaker now, and coherence has become something a team must defend deliberately or lose.\n\n## Write down the small number of ways\n\nOne page, in the repository, listing how this codebase does the recurring things. Not a style guide about brace placement: the decisions that matter and recur.\n\n```markdown\n# How we do things here\n\nBackground work:      Celery, via `tasks/`. Not threads, not cron, not\n                      a queue in the database.\nHTTP calls out:       `lib/http.py` only. It sets timeouts and retries.\nConfiguration:        environment variables, parsed once in `config.py`.\n                      No `os.environ` anywhere else.\nDatabase access:      repository classes in `repo/`. Handlers never write SQL.\nErrors to the user:   raise a subclass of `UserFacingError`; the middleware\n                      formats it. Never build an error response by hand.\nMoney:                integer minor units, `Money` type, never float.\nTime:                 timezone-aware UTC everywhere; convert at the edge only.\n```\n\nEight lines. It is the most useful page in most repositories, it takes an hour to write, and it does double duty now: it tells your colleagues and it tells the tools, since it belongs in whatever conventions file your editor reads.\n\n## Then make a machine enforce it\n\nA written convention decays. An automated one does not. These checks are called fitness functions and they are cheaper to set up than most people expect.\n\n```ini\n# Python: import-linter, in setup.cfg\n[importlinter:contract:layers]\nname = Layered architecture\ntype = layers\nlayers =\n    api\n    services\n    repo\n```\n\nThat contract fails the build if `repo` ever imports from `api`. One config block, permanent enforcement, and it catches the change that would slowly invert your dependency direction.\n\n```javascript\n// JavaScript: dependency-cruiser\nforbidden: [{\n  name: \"no-ui-in-domain\",\n  from: { path: \"^src/domain\" },\n  to:   { path: \"^src/(ui|components)\" },\n  severity: \"error\"\n}]\n```\n\nOthers in the same family, all free: ArchUnit for Java and .NET, `eslint-plugin-boundaries`, Rust's module visibility, Go's internal packages. And the crudest version, which works everywhere and takes two minutes:\n\n```bash\n# fail if anything outside lib/ imports requests directly\n! rg -l '^import requests' --glob '!lib/**' src/\n```\n\nA grep in CI is not elegant and it holds the line for years.\n\n## Give each area an owner\n\n`CODEOWNERS` is one file and it puts a named human on every change to a sensitive area.\n\n```\n/billing/          @ananya @ravi\n/auth/             @security-team\npackage-lock.json  @platform-team\n/docs/adr/         @tech-leads\n```\n\nThis is not gatekeeping. It is the mechanism by which somebody who holds the mental model of an area sees every change to it, which is exactly what stops the fifteen-ways problem, because that person will notice the sixteenth way when nobody else would.\n\n## Where the human judgement has to stay\n\nAutomation catches violations of decisions already made. It cannot make the decision.\n\nWhich means the architectural questions (should this be a separate service, what is the boundary between these two modules, do we need a queue here, is this the right data model) stay with people, and should stay with people, because they are bets on what your organisation does next.\n\nA useful test for whether a decision is delegable: **would being wrong be visible within a week?** If yes, decide fast and cheaply and fix it if wrong. If the cost of being wrong shows up in eighteen months as a system nobody can change, that is a decision for someone who will still be there and who understands what the business is trying to become.\n\n## The property worth tracking\n\nOne question, asked of each service every quarter: **can anybody on this team explain how this works?**\n\nName them. If the answer is nobody, that is an operational risk of the same kind as an unmonitored backup, and it should be on the same list. It is also the earliest measurable form of the coherence problem, because a system with fifteen ways of doing things is precisely a system that nobody can hold in their head."},"check":{"question":"A team documents its layering rules in a well-written architecture guide. A year later, handlers routinely contain SQL. What was missing?","options":[{"text":"Ownership, since without CODEOWNERS entries no experienced person was reviewing changes to those handlers.","correct":false,"tempting":"Owners help enormously, but they are humans reviewing under time pressure: the rule still decays without a check that cannot be tired."},{"text":"A mechanism that fails when the rule is broken, rather than a document describing it.","correct":true},{"text":"Training, since the guide existed but nobody joining the team was walked through it during onboarding.","correct":false,"tempting":"Onboarding raises awareness, which is exactly what the guide already did; awareness is not what prevents a shortcut at 5pm."},{"text":"The layering rule was too strict to be practical, so people worked around it and the guide should have been revised.","correct":false,"tempting":"Sometimes true and worth checking, but a rule abandoned silently rather than revised deliberately is the failure being described."}]},"prev":{"slug":"consolidation-and-entropy","title":"Scheduling the deletion","url":"https://addaly.com/learn/how-software-changed/consolidation-and-entropy"},"next":{"slug":"onboarding-without-an-author","title":"Onboarding into code nobody wrote","url":"https://addaly.com/learn/how-software-changed/onboarding-without-an-author"},"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) 'Coherence has to be defended by something', in How Software Development Changed. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/how-software-changed/architectural-coherence (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":"Coherence has to be defended by something","courseTitle":"How Software Development Changed","module":{"index":6,"slug":"teams-and-delivery","title":"Teams, process and delivery"},"answer":null,"keyIdea":"No individual diff is where architectural drift gets decided, so coherence needs a written page of how this codebase does recurring things plus an automated fitness function that fails the build, while the genuinely architectural bets stay with people.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"nothing-objects-to-a-system-acquiring-fifteen-ways-to-do-one","runs":[{"text":"Nothing objects to a system acquiring fifteen ways to do one thing"}]},{"t":"p","runs":[{"text":"Generated code is excellent locally and indifferent globally. Each change fits its immediate surroundings. No individual diff is the place where “we now have three approaches to background work” gets decided, so it never gets decided: it accumulates."}]},{"t":"p","runs":[{"text":"This was previously held together by two accidents. Writing was slow, so people reused rather than rewrote. And a small number of people wrote most of the code, so their preferences propagated by default. Both accidents are weaker now, and coherence has become something a team must defend deliberately or lose."}]},{"t":"h2","id":"write-down-the-small-number-of-ways","runs":[{"text":"Write down the small number of ways"}]},{"t":"p","runs":[{"text":"One page, in the repository, listing how this codebase does the recurring things. Not a style guide about brace placement: the decisions that matter and recur."}]},{"t":"code","lang":"markdown","label":"Markdown","text":"# How we do things here\n\nBackground work:      Celery, via `tasks/`. Not threads, not cron, not\n                      a queue in the database.\nHTTP calls out:       `lib/http.py` only. It sets timeouts and retries.\nConfiguration:        environment variables, parsed once in `config.py`.\n                      No `os.environ` anywhere else.\nDatabase access:      repository classes in `repo/`. Handlers never write SQL.\nErrors to the user:   raise a subclass of `UserFacingError`; the middleware\n                      formats it. Never build an error response by hand.\nMoney:                integer minor units, `Money` type, never float.\nTime:                 timezone-aware UTC everywhere; convert at the edge only."},{"t":"p","runs":[{"text":"Eight lines. It is the most useful page in most repositories, it takes an hour to write, and it does double duty now: it tells your colleagues and it tells the tools, since it belongs in whatever conventions file your editor reads."}]},{"t":"h2","id":"then-make-a-machine-enforce-it","runs":[{"text":"Then make a machine enforce it"}]},{"t":"p","runs":[{"text":"A written convention decays. An automated one does not. These checks are called fitness functions and they are cheaper to set up than most people expect."}]},{"t":"code","lang":"ini","label":"INI","text":"# Python: import-linter, in setup.cfg\n[importlinter:contract:layers]\nname = Layered architecture\ntype = layers\nlayers =\n    api\n    services\n    repo"},{"t":"p","runs":[{"text":"That contract fails the build if "},{"text":"repo","code":true},{"text":" ever imports from "},{"text":"api","code":true},{"text":". One config block, permanent enforcement, and it catches the change that would slowly invert your dependency direction."}]},{"t":"code","lang":"javascript","label":"JavaScript","text":"// JavaScript: dependency-cruiser\nforbidden: [{\n  name: \"no-ui-in-domain\",\n  from: { path: \"^src/domain\" },\n  to:   { path: \"^src/(ui|components)\" },\n  severity: \"error\"\n}]"},{"t":"p","runs":[{"text":"Others in the same family, all free: ArchUnit for Java and .NET, "},{"text":"eslint-plugin-boundaries","code":true},{"text":", Rust’s module visibility, Go’s internal packages. And the crudest version, which works everywhere and takes two minutes:"}]},{"t":"code","lang":"bash","label":"Bash","text":"# fail if anything outside lib/ imports requests directly\n! rg -l '^import requests' --glob '!lib/**' src/"},{"t":"p","runs":[{"text":"A grep in CI is not elegant and it holds the line for years."}]},{"t":"h2","id":"give-each-area-an-owner","runs":[{"text":"Give each area an owner"}]},{"t":"p","runs":[{"text":"CODEOWNERS","code":true},{"text":" is one file and it puts a named human on every change to a sensitive area."}]},{"t":"code","lang":"","label":"Text","text":"/billing/          @ananya @ravi\n/auth/             @security-team\npackage-lock.json  @platform-team\n/docs/adr/         @tech-leads"},{"t":"p","runs":[{"text":"This is not gatekeeping. It is the mechanism by which somebody who holds the mental model of an area sees every change to it, which is exactly what stops the fifteen-ways problem, because that person will notice the sixteenth way when nobody else would."}]},{"t":"h2","id":"where-the-human-judgement-has-to-stay","runs":[{"text":"Where the human judgement has to stay"}]},{"t":"p","runs":[{"text":"Automation catches violations of decisions already made. It cannot make the decision."}]},{"t":"p","runs":[{"text":"Which means the architectural questions (should this be a separate service, what is the boundary between these two modules, do we need a queue here, is this the right data model) stay with people, and should stay with people, because they are bets on what your organisation does next."}]},{"t":"p","runs":[{"text":"A useful test for whether a decision is delegable: "},{"text":"would being wrong be visible within a week?","b":true},{"text":" If yes, decide fast and cheaply and fix it if wrong. If the cost of being wrong shows up in eighteen months as a system nobody can change, that is a decision for someone who will still be there and who understands what the business is trying to become."}]},{"t":"h2","id":"the-property-worth-tracking","runs":[{"text":"The property worth tracking"}]},{"t":"p","runs":[{"text":"One question, asked of each service every quarter: "},{"text":"can anybody on this team explain how this works?","b":true}]},{"t":"p","runs":[{"text":"Name them. If the answer is nobody, that is an operational risk of the same kind as an unmonitored backup, and it should be on the same list. It is also the earliest measurable form of the coherence problem, because a system with fifteen ways of doing things is precisely a system that nobody can hold in their head."}]}],"check":{"question":[{"text":"A team documents its layering rules in a well-written architecture guide. A year later, handlers routinely contain SQL. What was missing?"}],"options":[{"text":[{"text":"Ownership, since without CODEOWNERS entries no experienced person was reviewing changes to those handlers."}],"correct":false,"tempting":[{"text":"Owners help enormously, but they are humans reviewing under time pressure: the rule still decays without a check that cannot be tired."}]},{"text":[{"text":"A mechanism that fails when the rule is broken, rather than a document describing it."}],"correct":true,"tempting":null},{"text":[{"text":"Training, since the guide existed but nobody joining the team was walked through it during onboarding."}],"correct":false,"tempting":[{"text":"Onboarding raises awareness, which is exactly what the guide already did; awareness is not what prevents a shortcut at 5pm."}]},{"text":[{"text":"The layering rule was too strict to be practical, so people worked around it and the guide should have been revised."}],"correct":false,"tempting":[{"text":"Sometimes true and worth checking, but a rule abandoned silently rather than revised deliberately is the failure being described."}]}],"order":[3,0,2,1],"why":null},"prev":{"slug":"consolidation-and-entropy","title":"Scheduling the deletion","minutes":9},"next":{"slug":"onboarding-without-an-author","title":"Onboarding into code nobody wrote","minutes":8}}}