When the Code Becomes the Spec
You started the project with good intentions. A software design document. An architecture diagram. A set of user stories with acceptance criteria. The team understood the plan. Development began.
Six months later, the code does something the SDD never described. The architecture diagram shows a boundary that no longer exists. The acceptance criteria reference a field the API dropped in sprint four. Nobody updated the documents. Nobody had time. The code moved faster than the paperwork.
This is code drift. In greenfield projects it happens earlier, faster, and more quietly than most teams expect.
Why greenfield projects are especially vulnerable
The assumption is that brownfield projects drift. They carry years of accumulated decisions, undocumented changes, and organisational memory that never made it into writing. Greenfield projects, the thinking goes, start clean. The spec is current. The team is aligned. The architecture is intentional.
That is true for the first few weeks.
Greenfield development moves quickly — that is the point. Decisions get made in stand-ups, in pull request comments, in Slack threads at 10pm. A field gets renamed because it conflicted with a reserved keyword. An endpoint gets split because the original design was too coarse. A dependency gets replaced because the first choice had a licensing problem. Each change is small. Each change is reasonable. None of them make it back into the SDD.
By the time the first real feature ships, the document describing the system is already a historical artefact rather than a current one.
The three most common forms of drift
The renamed thing
The SDD describes a valuation object with a submittedAt timestamp. The code has an assessment object with a createdOn timestamp. At some point someone renamed it to better reflect the domain language that emerged during implementation. The renaming was the right call. But the SDD still says valuation and submittedAt. Anyone reading the document to understand the system is reading about a thing that no longer exists by that name.
This form of drift is benign in isolation. When it accumulates across dozens of fields, objects, and endpoints, it makes the SDD useless as a reference document. The team stops reading it because they know it is wrong.
The missing constraint
The SDD specifies that a property valuation requires a completed compliance check before submission. During implementation the team discovered that compliance checks are asynchronous. Rather than block the submission flow, they made compliance optional at submission time and enforced it at a later stage in the workflow. That was a pragmatic implementation decision. It was also a change to a business rule that never made it back into the document.
The SDD now says one thing. The code does another. An engineer reading the SDD to understand the compliance flow will misunderstand how it actually works. A test written against the SDD will be testing a behaviour the system does not have.
This form of drift is not benign — it represents a business rule that exists in one version in the documentation and a different version in the code. Whether the code version or the document version is correct is a question that can only be answered by a business stakeholder, and the team may not even know the question exists.
The emergent boundary
The SDD described three services communicating over REST. During development a fourth communication pattern emerged: one service started writing events to a message queue that another service reads. Nobody designed this in the original architecture. It solved a real problem. It was a good idea. It was never added to the architecture document.
The system now has an integration boundary that exists nowhere in the official description of the system. There are no tests for it, because the people writing tests were working from the SDD. There is no contract for it, because nobody defined one. It works until something changes on either side, at which point it fails silently.
This form of drift is the most dangerous. It creates real integration risk from an undocumented, untested contract.
Why the spec update comes last
The root cause is straightforward. Writing code is satisfying. It produces something you can see and run and demo. Updating documentation produces nothing immediately visible. When the choice is between writing the next feature and updating the SDD to reflect the last one, the feature wins. Every time. Under deadline pressure it is not even a close call.
This is not a discipline problem. It is an incentive problem. The reward for writing code is immediate. The reward for maintaining documentation is delayed, diffuse, and mostly invisible until something goes wrong. No one gets praised in a sprint retrospective for keeping the SDD current. Plenty of people get blamed when someone acts on stale documentation and produces the wrong result.
Teams that ship the implementation and leave the spec for later are not being lazy. They are being rational, given the incentives they are working under: the spec update feels like overhead on work that is already done, and the implementation feels like the real work.
The real problem shows up later — in what accumulates over dozens of these choices across a six-month project.
What drift looks like from the outside
From a project management perspective, a drifted greenfield project looks healthy right up until it does not. Coverage numbers are high because the tests were written against the code that exists, not the spec that was intended. CI is green because the tests pass the implementation they were written to test. The SDD is filed somewhere and last opened weeks ago.
The first sign of drift is usually a new team member who tries to use the SDD to understand the system and discovers it does not match what they see in the code. The second sign is a test failure nobody can explain, because the test was written against a constraint that was silently removed from the implementation. The third sign is a production incident at an integration boundary nobody knew was undocumented.
None of these are catastrophic in isolation. Together they are the signature of a codebase that has outpaced its own documentation.
The test suite as an accidental specification
There is an irony at the centre of this problem. The test suite, written against the implementation rather than the spec, becomes more accurate than the SDD over time. The tests describe what the system actually does. The SDD describes what someone intended it to do at a point in the past.
That is not entirely bad — a test suite that accurately describes current behaviour is a useful asset, a form of living documentation. But it has an important limitation: it only describes the behaviour someone thought to test. The missing constraint, the emergent boundary, the renamed field that changed semantics along with its name — these are invisible to a test suite written to confirm what the code does rather than to verify what the business requires.
The test suite can tell you the code is internally consistent. It cannot tell you the code is correct against an external requirement it was never tested against.
The detection problem
The practical challenge is that code drift in a greenfield project is largely invisible to the tools most teams rely on. Static analysis measures code quality. It does not know what the SDD said. CI reports pass or fail based on the tests that exist. It does not know what tests should exist based on the original specification.
The only way to surface drift is to compare what the code does against what the specification says it should do, across the full implementation, systematically. That is expensive to do manually. A developer asked to reconcile a six-month codebase against a fifty-page SDD will either do it superficially or not do it at all.
The more tractable approach is to make drift visible at the boundary level rather than at the document level. Rather than comparing the full codebase to the full SDD, identify the integration boundaries that exist in the code and compare them against the integration boundaries in the specification.
Boundaries that exist in the code but not the spec are emergent. Boundaries that exist in the spec but not the code are either deferred or silently dropped. Boundaries that exist in both can be checked for contract alignment.
This surface is smaller and more tractable than a full document reconciliation, and it catches the most consequential class of drift — the kind that creates undocumented integration risk.
The business rule problem is harder
The missing constraint problem is harder to detect automatically, because business rules are often not encoded in integration boundaries. They live in service logic, in validation code, in conditional branches that implement a policy the SDD described but the tests never verified.
The only reliable detection mechanism for this class of drift is the combination of two things: a characterization step that documents what the code currently does at the module level, and a comparison between that characterization and the stated requirements. Where the two diverge, a human has to decide which version is correct.
That is not something a tool can resolve. It is something a tool can surface. The value is in making the question visible rather than letting it stay hidden until it causes a production incident or an audit failure.
What teams can do about it
The honest answer is that preventing drift entirely is not realistic — the incentive structure, the pace of development, and the deadline pressure are all pulling the other way.
What is realistic is making drift visible earlier and cheaper to address.
Treat integration boundaries as a first-class deliverable. When a new integration boundary is added during implementation, update the architecture document before the pull request is merged. This is a two-minute task if the boundary is fresh. It is a two-hour archaeology exercise if it is discovered six months later. Making the boundary update part of the definition of done for any PR that introduces or changes one costs almost nothing per PR and eliminates the emergent boundary class of drift entirely.
Write tests against requirements, not just against code. A test suite that only verifies what the code does cannot detect when the code diverges from what it should do. Adding at least a small set of tests that verify specific business rules stated in the SDD, regardless of the implementation details, gives the team an automated signal when a rule is silently violated. These tests will sometimes fail not because the code is wrong but because the requirement changed and the test needs updating. That failure is valuable information.
Review the SDD as part of sprint retrospectives. Not every section of the document — just the sections touched that sprint. A fifteen-minute SDD review in the retrospective, covering only the features delivered that sprint, catches the renaming drift and the missing constraint drift before they become a month's worth of accumulated difference. The cost is fifteen minutes per sprint. The payoff is a document that stays readable and accurate throughout the project.
Treat discrepancies as questions, not as errors. When the code diverges from the spec, the instinct is to fix one of them without asking which one is correct. Resist that. Each discrepancy is a question: does the code reflect a legitimate implementation decision, or does it reflect a constraint the business still needs that the implementation dropped? That question belongs to a business stakeholder, not to the developer who discovers the discrepancy. Making it a question rather than an assumption is what keeps the business-rule mismatch from becoming invisible.
The bigger picture
Code drift is not a failure of any individual. It is the natural result of a development process that treats documentation as overhead rather than as a first-class output of the work. In a world where features ship weekly and deadlines are real, keeping a specification current is genuinely difficult.
The teams that manage it best are not necessarily the most disciplined ones. They have simply found ways to make staying current cost less than falling behind — boundary-level updates on every PR, requirement-level tests for the most important business rules, and brief spec reviews as part of the existing retrospective rhythm.
The goal is not a perfect SDD. The goal is a codebase where the things that matter most — the integration contracts, the business rules, the security constraints — remain visible and testable even as the implementation evolves. Everything else can be inferred from the code. These cannot.
This article draws on patterns seen across ToolTwist's brownfield engineering engagements — the same drift this piece describes, caught earlier and cheaper.