<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Leon Pennings</title>
    <description>The latest articles on DEV Community by Leon Pennings (@leonpennings).</description>
    <link>https://dev.to/leonpennings</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3596884%2Feba64cf4-e1c3-4a53-8a5f-6a340619080e.JPG</url>
      <title>DEV Community: Leon Pennings</title>
      <link>https://dev.to/leonpennings</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/leonpennings"/>
    <language>en</language>
    <item>
      <title>Why Software Can't Tell You It's Wrong</title>
      <dc:creator>Leon Pennings</dc:creator>
      <pubDate>Wed, 08 Jul 2026 06:52:26 +0000</pubDate>
      <link>https://dev.to/leonpennings/why-software-cant-tell-you-its-wrong-3l8i</link>
      <guid>https://dev.to/leonpennings/why-software-cant-tell-you-its-wrong-3l8i</guid>
      <description>&lt;p&gt;Software architecture debates have a problem that most other engineering disciplines don't: the alternative was never built.&lt;/p&gt;

&lt;p&gt;When a bridge fails, the failure is physical, attributable, and measurable against every other bridge that didn't. The engineering decisions that caused it can be isolated, traced, and corrected — not just in theory, but in the next bridge, because the material itself produces feedback that no amount of professional opinion can override. Steel deflects. Concrete cracks. Physics doesn't care what the architect believed.&lt;/p&gt;

&lt;p&gt;Software produces no equivalent feedback. A system built around the wrong abstractions compiles, runs, ships, and passes its tests just as readily as one built around the right ones. A bug introduced by a misaligned domain model looks identical, from the outside, to a bug introduced by a typo. A feature that took three times longer than it should have, because the structure made it harder than the business logic warranted, produces no artifact that distinguishes it from a feature that was simply difficult. The cost is real. The cause is invisible.&lt;/p&gt;

&lt;p&gt;This is the unfalsifiability problem, and it runs deeper than "we can't measure everything." It means that when a system becomes expensive to change, the diagnosis almost always lands on the wrong variable. The domain is complex. The requirements changed. The previous team was careless. Almost never: the structure was wrong, and the structure was wrong because nobody ever built the other version of it to compare against. That version doesn't exist, it never will, and every architectural argument in the industry is conducted in its absence.&lt;/p&gt;

&lt;p&gt;This would be a purely philosophical problem if there were nothing to do about it. There is something to do about it — but it requires accepting that the standard metric for software quality, whether it works, is measuring the wrong thing entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Metric That Hides the Problem
&lt;/h2&gt;

&lt;p&gt;The natural substitute for "is this good engineering" is "does it work," because working is the only property the system demonstrates directly and continuously. A feature either satisfies the requirement or it doesn't. Tests are either green or they aren't. The sprint either shipped or it didn't. These are checkable facts, and in an environment with no physical feedback to measure against, checkable facts become the entire basis for evaluation.&lt;/p&gt;

&lt;p&gt;The problem is that working is compatible with almost any underlying structure, good or bad, for a surprisingly long time. A system can satisfy every functional request in its backlog while its essential complexity — the actual rules and relationships of the business — is scattered across service methods, duplicated across layers, encoded differently in three places because three developers made the same inference independently without any structural pressure to put it in one place. None of that appears as a failure. It appears as a working system, until the day a new requirement needs to reach across all three of those places simultaneously, and what should be a small change becomes an investigation into how many versions of the rule actually exist and which ones are authoritative.&lt;/p&gt;

&lt;p&gt;By then the cause is undiagnosable. The system is large. The rule has accreted across two years of sprint reviews. The developers who wrote the original three versions are gone. The structure that caused the problem will be attributed to enterprise complexity, changing requirements, accumulated technical debt — anything except the decision, made early and silently, to let working stand in for well-structured.&lt;/p&gt;

&lt;p&gt;When behavior is the only metric, behavior is what gets optimized. Frameworks that produce correct behavior fast become the natural answer. Templates that generate working code without requiring structural reasoning become best practice. The question that would actually determine the ten-year cost — what are the mechanics that produce this behavior, and do those mechanics live in a place that makes them findable, correctable, and legible — never gets asked, because asking it requires a comparison that isn't available.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mechanics, Not the Behavior
&lt;/h2&gt;

&lt;p&gt;The substitute for physical falsifiability isn't a better way to measure behavior. It's a deliberate shift in what gets examined — from the surface of the system, what it does, to the structure underneath it, what it is.&lt;/p&gt;

&lt;p&gt;Every piece of business behavior is produced by mechanics: relationships between concepts, rules that govern those relationships, responsibilities that belong to one concept and not another. A checkout process is behavior. The mechanics underneath it are what actually need to be represented correctly.&lt;/p&gt;

&lt;p&gt;Consider a library lending system. The behavior is simple: a borrower takes an item, returns it later. The mechanic, examined carefully, is that a loan is a distinct event — something that happened between a person and an item, at a point in time, with its own start, end, and history. It is not a property of the item itself. A book does not become a different kind of thing when it is borrowed. The loan is a separate concept with a separate lifecycle. That distinction is not an implementation preference. It is what is actually true about the business — and a system that puts loan dates on the Book entity has encoded a false mechanic, one that will prevent the system from answering basic questions about lending history without structural rework.&lt;/p&gt;

&lt;p&gt;The behavior can be implemented many ways. The mechanic is either represented correctly or it isn't, and unlike behavior, a misrepresented mechanic produces a specific, locatable kind of wrongness: the rule exists in the wrong place, the concept carries responsibility it was never supposed to own, the assumption is encoded somewhere it can't be found when it turns out to be wrong.&lt;/p&gt;

&lt;p&gt;This is not a theoretical exercise. It is a structuring and learning activity. The goal is not to produce a perfect model before a line of code is written — it is to make the structure of the business explicit enough that building it becomes a matter of finding the right place for each piece, rather than making an independent decision every time about where things should go. A user story is not a specification to implement. It is information about what the business is — a piece of evidence that either fits the current understanding of the mechanics or reveals that the understanding was incomplete. Both outcomes are useful. The first confirms the model. The second improves it.&lt;/p&gt;

&lt;p&gt;Once the mechanics are explicit, simplicity becomes achievable in a way it never is when the focus stays on behavior alone. The simplest thing that works is not the simplest thing to type — it is the implementation that puts each responsibility exactly where it belongs, with no duplication, no misdirection, no logic living in a place that requires explanation rather than being found by intuition. That kind of simplicity is legible to a new developer on their first day. It is transferable to a team member who didn't write it. It is correctable when an assumption turns out to be wrong, because the assumption lives in one place that can be questioned directly rather than in a diffuse implication scattered across the system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Essential and Accidental Complexity
&lt;/h2&gt;

&lt;p&gt;Fred Brooks named this distinction decades ago, and it has lost none of its precision: essential complexity is the difficulty that comes from what the problem actually is; accidental complexity is the difficulty introduced by the tools and representations chosen to solve it.&lt;/p&gt;

&lt;p&gt;Essential complexity cannot be removed without changing the problem. The fact that a loan involves a lender, an item, and a time period — and that these are three separate things with separate lifecycles — is not a decision anyone made about how to model a library system. It is what a loan is. Strip it out and the system no longer represents what the business does. Every piece of structure that reflects this accurately is essential complexity made explicit.&lt;/p&gt;

&lt;p&gt;Accidental complexity is everything else: the framework, the deployment topology, the persistence layer, the test harness, the CI pipeline, the service boundaries drawn for organizational rather than domain reasons. None of this exists in the business. All of it exists to make the essential complexity operational. It is supposed to serve the essential complexity, not the other way around. The moment a framework's conventions, a deployment constraint, or an organizational boundary starts dictating what the domain is allowed to look like, accidental complexity has taken the lead, and the essential complexity is being shaped to fit it rather than the reverse.&lt;/p&gt;

&lt;p&gt;This inversion is the likely trajectory of most software projects within a few years of starting — unfalsifiability means it can be neither proven nor disproven with certainty, but a perfect structural alignment between code and business mechanics from day one, sustained without a mechanism to detect and correct drift, requires a precision that makes winning the lottery look achievable. It happens quietly because it produces no visible symptom. The system still works. The tests still pass. The sprints still ship. The essential complexity didn't disappear — it was dissolved into the accidental complexity surrounding it, encoded in service method names and transaction annotations, still doing its job but no longer findable as a thing you can look at, question, or change in one place.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cogwheels of the Business
&lt;/h2&gt;

&lt;p&gt;A mechanical watch is a useful image here. The cogwheels of a watch are its essential complexity made physical — the gear ratios, the relationships between wheels, the escapement that governs the release of energy. These cannot be simplified away without the watch losing the ability to tell time. They are what the watch fundamentally is. The casing, the hands, the crystal, the materials — these are accidental complexity: real, necessary, but entirely in service of the mechanism underneath. A watchmaker looking at a malfunctioning watch does not need to guess where the problem is. The mechanism is visible. Each part has a known responsibility, and a part that has the wrong shape for its position will produce a specific, locatable failure rather than a general degradation of the whole. The casing does not malfunction. The mechanism does — and it can be found.&lt;/p&gt;

&lt;p&gt;A rich domain model is the cogwheels of the business — and the automation of it. Not data with operations attached, which is what most systems produce, but responsibilities that carry their own data and enforce their own rules. An account is not a row in a table with balance and status fields. An account is something a customer has, that carries a balance, that knows what it means to deposit and withdraw, that enforces the rules governing when a withdrawal is and isn't permitted. Those rules belong on the account — not because of aesthetic preference, but because that is where they would be found by anyone who thought to look for them by asking what an account is. When those rules live on the account, a wrong rule is a local problem, visible and correctable in one place. When those rules live in a &lt;code&gt;WithdrawalService&lt;/code&gt; and a &lt;code&gt;BalanceCheckService&lt;/code&gt; and a &lt;code&gt;TransactionValidationService&lt;/code&gt; distributed across a microservice boundary, a wrong rule is a distributed problem, findable only by someone who already knows which services encode the assumption and can read them all simultaneously.&lt;/p&gt;

&lt;p&gt;The practical difference is not academic. On a system where the essential complexity is explicit, a new developer can read the model, understand what the business is, and contribute meaningfully in days — because the structure of the code reflects the structure of the business, and understanding one is understanding the other. On a system where the essential complexity has been dissolved into accidental layers, a new developer learns how to navigate the framework, traces several services to reconstruct a rule nobody wrote down, makes a change, breaks something else that encoded the same rule differently, and starts the cycle again. The business is still there — it is just no longer visible as the thing organizing the code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Correctability as the Partial Answer
&lt;/h2&gt;

&lt;p&gt;This is where the unfalsifiability problem becomes something other than a purely philosophical concern.&lt;/p&gt;

&lt;p&gt;Unfalsifiability means the alternative was never built, so no direct comparison is available. It does not mean nothing can be done. The absence of a physical feedback mechanism can be partially compensated for by a structural property: if wrong assumptions live in one legible place, they can be corrected when they turn out to be wrong. Correctability does not prove the original model was wrong — it cannot, because unfalsifiability is still in effect. But it prevents the cost of being wrong from compounding indefinitely, which is the actual damage the problem causes. A wrong assumption that gets corrected in year two costs a week. The same wrong assumption that is only discoverable by reading fourteen service methods, in year five, after three team changes, costs months — and the correction itself has to find and fix every place the assumption was encoded, which are no longer guaranteed to be consistent with each other.&lt;/p&gt;

&lt;p&gt;When essential complexity is kept explicit, a misaligned model is a correctable one. When it has been dissolved into accidental layers, a misaligned model is a permanent one — not because it cannot technically be corrected, but because the correction cost grows faster than anyone is willing to pay it, and the system calcifies around the wrong assumptions instead. Every new feature gets built on top of them. Every new developer inherits them. The system is not failing. It is just becoming progressively more expensive to change, in a way nobody can trace back to a cause because the cause is structural and the structure is no longer visible.&lt;/p&gt;

&lt;p&gt;Focusing only on whether the system works skips this entirely. Frameworks that deliver working behavior without requiring the essential complexity to be made explicit appear to win the short-term comparison — they are easier to hire for, and they produce green tests by the end of the sprint. But the appearance is produced by measuring the wrong thing.&lt;/p&gt;

&lt;p&gt;A sprint review that demonstrates two completed screens and a passing test suite looks productive. The question it doesn't answer is what those two screens cost in accidental complexity — in configuration, wiring, DTO mapping, coverage mandates, and refinement sessions spent on how to make the technology work rather than on what the business actually needs. A team working from an explicit model of the essential complexity spends that time differently: on understanding what the business is, on placing each new piece of behavior where it belongs, on confirming or correcting the model. The visible output per sprint may look similar. The structural cost is not.&lt;/p&gt;

&lt;p&gt;This is worth qualifying honestly. As long as a codebase is small enough to fit inside the working context of the team maintaining it, the benefit of explicit essential complexity is real but modest — the system is comprehensible regardless, because it is simply small enough to hold in one mind. The benefit scales with the size and longevity of the system. As the application grows, without explicit essential complexity, the effort required just to maintain an overview grows faster than the team can absorb it. The common response is distribution — splitting the system into smaller, independently manageable pieces. But distribution assumes precise knowledge of where the business can be cleanly separated, and that knowledge is exactly what the unfalsifiability problem prevents anyone from having with confidence. The separation points get drawn on organizational lines, on deployment convenience, on what seemed coherent at the time — and every cross-cutting rule the business later needs, every requirement that turns out to span the boundary that was drawn too early, pays the price of an integration problem that a single coherent model would never have generated.&lt;/p&gt;

&lt;p&gt;The cost of dissolving the essential complexity into accidental layers does not begin at year three. It begins at the first sprint, accumulates without attribution, and will eventually be diagnosed as enterprise complexity or changing requirements or the inevitable entropy of large systems — not as the consequence of a structural choice made at the beginning, when the essential complexity was allowed to dissolve rather than being required to remain visible.&lt;/p&gt;

&lt;p&gt;The cogwheels of the business are either explicit or they are not. If they are, the system can tell you when it is wrong. If they are not, it cannot — and the bill for that silence will arrive, as it always does, long after the people who made the choice have moved on.&lt;/p&gt;




&lt;h2&gt;
  
  
  So What
&lt;/h2&gt;

&lt;p&gt;There are two ways to build enterprise software, and the choice between them is made — usually implicitly, usually early — by whoever decides what success looks like.&lt;/p&gt;

&lt;p&gt;The first focuses on what the system does. Tools, frameworks, and delivery velocity are the primary concerns. User stories are work orders. The sprint is the unit of value. A working feature at the end of the iteration is the definition of done. This approach has a ceiling that is invisible until it has already been reached: the essential complexity of the business accumulates inside accidental layers, the system ages with its technology choices, new insights are hard to incorporate because nobody can find where the old ones live, and distribution gets introduced to manage the sprawl — which requires precise knowledge of where the business can be cleanly separated, knowledge that by definition is not yet available. The result is software that works for years while becoming progressively more expensive to change, until the cost of a new feature is indistinguishable from the cost of a small project, and the organisation starts talking about a rewrite.&lt;/p&gt;

&lt;p&gt;The second focuses on what the system is. The essential complexity of the business — the actual rules, relationships, and responsibilities — is made explicit, kept in one place, and treated as the primary asset. Technology serves it. Frameworks are evaluated against it. New requirements are treated as information about it. When an assumption turns out to be wrong, it can be found and corrected, because it was never allowed to dissolve into the layers surrounding it. This approach does not age with its technology choices, because the technology was never load-bearing. It does not require distribution to stay manageable, because the model provides the coherence that distribution was trying to restore. New developers find the system comprehensible because the structure of the code reflects the structure of the business, and understanding one is understanding the other.&lt;/p&gt;

&lt;p&gt;The first approach feels more immediately productive because it generates visible artefacts — configured services, passing tests, completed screens — while deferring the understanding that would have made those artefacts cheaper to produce. The second does the understanding first, which means the first sprint looks quieter and delivers more.&lt;/p&gt;

&lt;p&gt;The cost difference between the two, accumulated over a decade of a business-critical application, is not marginal. It is the kind of number that makes a CFO ask why nobody mentioned this was a choice. The real jewels of enterprise software are not in the framework versions, the language features, or the architectural patterns that fill conference schedules every year. They are in language basics that have not changed in thirty years — an object that knows what it is, owns what belongs to it, and enforces its own rules. Everything else is the casing. The mechanism is what keeps the time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is part of a series on software engineering craft. Other pieces examine the rich domain model as a discipline, the properties of enterprise software that lasts, why frameworks answer structural questions before the domain gets a chance to, and why the practices that prevent structural decay are the same ones that always prevented it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>softwaredevelopment</category>
      <category>java</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The Software Bug AI Can't Find</title>
      <dc:creator>Leon Pennings</dc:creator>
      <pubDate>Fri, 26 Jun 2026 20:41:32 +0000</pubDate>
      <link>https://dev.to/leonpennings/the-software-bug-ai-cant-find-cc1</link>
      <guid>https://dev.to/leonpennings/the-software-bug-ai-cant-find-cc1</guid>
      <description>&lt;p&gt;Coding is not the hard part.&lt;/p&gt;

&lt;p&gt;Any reasonably skilled developer can implement a described requirement. Given a clear specification, the implementation follows. That has always been true, and AI has made it more true — the mechanical work of turning a description into working code is now faster and cheaper than it has ever been.&lt;/p&gt;

&lt;p&gt;The hard part is something different. It is thinking of everything that can go wrong. The data combination nobody anticipated. The sequence of operations that seemed impossible until a user found it. The edge case that only appears when two legitimate business scenarios collide in a way nobody modeled. These are not coding failures. They are imagination failures — and no developer, no team, no AI has ever been immune to them.&lt;/p&gt;

&lt;p&gt;The industry learned this lesson once before. Waterfall's central assumption was that requirements could be fully specified before building began — that if you thought hard enough upfront, you could anticipate everything. It couldn't be done. The act of building revealed what nobody knew before building started. Scenarios emerged from real usage that no specification session had surfaced. The industry eventually accepted this and moved on.&lt;/p&gt;

&lt;p&gt;The same assumption lives inside distributed architectures, one level down. You cannot anticipate every failure mode before the system meets real data. The question is not how to eliminate that gap — you cannot. The question is: when reality finds the gap, how fast does the system tell you?&lt;/p&gt;

&lt;p&gt;There are two possible answers. The system fails loudly — the operation stops, nothing partial is committed, the error is visible, the developer finds it, it gets fixed. Or the system fails silently — the operation appears to succeed, something partial is committed somewhere, the inconsistency enters the data, and nobody knows.&lt;/p&gt;

&lt;p&gt;Loud failure is not a side effect of good architecture. It is a feature — the mechanism by which a system corrects its own gaps as reality reveals them. It needs to be deliberately designed in. And a surprising number of the technology choices the industry has normalised over the last decade quietly design it out.&lt;/p&gt;

&lt;p&gt;Everything that follows is a consequence of that distinction.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Transaction Is Not a Technical Detail
&lt;/h2&gt;

&lt;p&gt;A single atomic transaction is the simplest possible implementation of loud failure.&lt;/p&gt;

&lt;p&gt;Something unexpected happens. The transaction fails. Everything inside the consistency boundary rolls back — the order wasn't created, the inventory wasn't reduced, the invoice wasn't generated. The state before the operation is restored exactly. The user sees an error. A developer looks at the error. They find the unconsidered scenario. They fix it. The feedback loop is hours, not months. The system's integrity was never compromised — just its availability, temporarily, for one specific operation.&lt;/p&gt;

&lt;p&gt;That is not a bug. That is the system working correctly under unexpected conditions — surfacing a gap in understanding at the cheapest possible moment, before anything was lost and before the inconsistency had a chance to compound.&lt;/p&gt;

&lt;p&gt;This is why technology choices for enterprise applications are not preferences. They are engineering decisions with structural consequences. A relational database brings three decades of battle-tested infrastructure for loud failure: non-nullable constraints, unique constraints, foreign key constraints, check constraints. These are not convenience features. They are a validation layer that lives closer to the data than any application code ever will, enforced regardless of which service forgot to set a field, regardless of which event handler failed to fire. The database simply refuses. Loudly. Immediately.&lt;/p&gt;

&lt;p&gt;Choosing to move away from a relational database is a legitimate engineering decision in specific circumstances. But it is not a neutral one. Every constraint the database was enforcing either moves into the application — where it is less reliable, harder to find, and maintained by people who may not know why it exists — or it disappears entirely, replaced by the hope that nobody will generate the data combination it was preventing. The validation does not vanish. It relocates, or it becomes invisible. Both outcomes are a step toward silent failure.&lt;/p&gt;

&lt;p&gt;Choosing a technology because it is popular, because a large company published a paper about it, because it appeared at a conference — without asking what properties it provides and what properties it removes — is not engineering. It is fashion. And in enterprise software, fashion has structural consequences that surface years later in production data nobody can explain.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Happens When You Distribute
&lt;/h2&gt;

&lt;p&gt;Now take the same unexpected scenario and run it through a distributed system.&lt;/p&gt;

&lt;p&gt;Service A processes its part and commits. An event fires. Service B receives it and fails — not because of bad code, but because this specific combination of data was never anticipated. Compensation logic can recover consistency, but only for the scenarios it was written to handle. Nobody wrote compensation logic for this combination, because nobody anticipated it. Service A has committed. Service B has not. The state is now inconsistent, and recovery now depends on logic whose correctness must itself be proven — for a scenario that, by definition, nobody saw coming.&lt;/p&gt;

&lt;p&gt;The user may not even see an error. The system appears to have worked.&lt;/p&gt;

&lt;p&gt;The inconsistency is now in production. Downstream services are making decisions based on it. Reports are being generated from it. Other operations are building on top of it. And nobody knows, because the system did not fail — it partially succeeded, which is the failure mode that distributed architectures are structurally unable to surface cleanly.&lt;/p&gt;

&lt;p&gt;Eighteen months later, someone notices the numbers don't add up. Or a customer calls about an order that shows as delivered but was never shipped. Or an audit finds financial records that contradict each other. The forensic work to trace that back to its origin — through eighteen months of events, across service boundaries, through compensation logic written by someone who left a year ago — is enormous. The fix is not a code change. It is a data integrity project, with permanent uncertainty about what the correct state actually was.&lt;/p&gt;

&lt;p&gt;The distributed system did not prevent the bug. It prevented the bug from being visible. Which is the worst possible trade — because loud failure is the mechanism the system uses to learn. Remove it and the system stops teaching. It just accumulates.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Part the "It Works" Argument Misses
&lt;/h2&gt;

&lt;p&gt;Here is where the reasonable objection comes in. Many distributed systems do work. Microservices applications run in production for years without the failure mode described above ever materialising. If yours is one of them, the argument so far probably seems theoretical.&lt;/p&gt;

&lt;p&gt;It is not theoretical. It is probabilistic — and the probability scales directly with the thing you most want to scale.&lt;/p&gt;

&lt;p&gt;Small application. Bounded domain. Limited entities, limited relationships, limited users, limited lifespan. The space of possible data combinations is small. The unconsidered scenario may simply never arrive before the system is retired. "It works" is genuinely true, start to finish. There is nothing to argue with.&lt;/p&gt;

&lt;p&gt;Now scale the application. More entities. More relationships. More users generating more combinations over more years. The space of possible data combinations grows faster than the team grows. The probability of hitting an unconsidered scenario does not stay constant — it compounds. At sufficient scale, over sufficient time, it stops being a risk and becomes a mathematical certainty.&lt;/p&gt;

&lt;p&gt;Which means the architectural choice that feels safe for a small system becomes a liability that scales directly with the size and longevity of the application. The system the organisation most wants to protect — the large, long-running, business-critical application — is exactly the system where silent failure becomes a certainty rather than a possibility.&lt;/p&gt;

&lt;p&gt;The developer who says "so what, it works" is describing a small system. They are right. They just don't realise that is what they are describing.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI Accelerates the Accumulation
&lt;/h2&gt;

&lt;p&gt;This is where the current moment makes the stakes undeniable.&lt;/p&gt;

&lt;p&gt;AI has the same imagination failure every human developer has. It implements what it was asked to implement. It does not anticipate the data combination that wasn't in the prompt. It does not model the collision between two legitimate business scenarios nobody thought to describe. And it generates code at a velocity that outpaces the domain understanding feeding it — accumulating unconsidered scenarios faster than any human team ever could.&lt;/p&gt;

&lt;p&gt;There is a subtler problem underneath that one. Writing code for a complex domain is not just implementation. It is how understanding develops. When a requirement does not fit cleanly, when the same logic appears in three places, when a method grows in ways that resist being read — that resistance is signal. The domain is surfacing a gap. The friction is the feedback loop by which an engineer's understanding deepens and the model gets corrected. AI used as an implementer absorbs that resistance. The code gets written. The discomfort never arrives. The lesson was in the discomfort.&lt;/p&gt;

&lt;p&gt;This is not a new failure mode. It is the continuation of a trend the industry has been on since framework-dictated development became the norm — where pre-packaged architectural recipes replaced structural reasoning, and engineers learned to fill in templates rather than interrogate structure. AI-as-implementer is the same dynamic, one abstraction level higher, running faster. The gap between working software and understood software was already widening before AI arrived. AI inherited that gap and accelerated it.&lt;/p&gt;

&lt;p&gt;In a system with a coherent consistency boundary, this matters less at the architectural level. The unconsidered scenario still fails loudly — AI-generated or not. The transaction fails, the error surfaces, the gap is found and fixed. The system remains self-correcting even when the engineer's understanding was incomplete.&lt;/p&gt;

&lt;p&gt;In a distributed system built at AI velocity, the unconsidered scenario fails silently — at a rate no previous generation of development ever achieved. The events queue. The inconsistencies compound. The data drifts. And the diagnosis, when it finally comes, will be what it always was: the domain was complex, the requirements changed, the previous developers were careless.&lt;/p&gt;

&lt;p&gt;Not: we built at a speed that outran our understanding, into an architecture that was designed to hide what we didn't know.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the Industry Got Here
&lt;/h2&gt;

&lt;p&gt;Nobody chose this deliberately. That is worth saying plainly before any diagnosis.&lt;/p&gt;

&lt;p&gt;Public software discourse is necessarily shaped by practices that can be taught, repeated, and verified at scale. The patterns that dominate conference talks, blog posts, job descriptions, and interview questions are the ones legible enough to transfer reliably between practitioners — not necessarily the ones that produce systems which remain coherent over a decade. That is not a criticism of the people involved. It is how knowledge disseminates in any field where the most consequential outcomes take years to become visible.&lt;/p&gt;

&lt;p&gt;And because every system is built once — the alternative approach is never built alongside it, so the cost of the wrong choice is never directly observable — the field cannot easily learn from its own experience. When a system develops data integrity problems, the cause gets attributed to domain complexity or changing requirements. Almost never does anyone conclude that the architecture was the variable, because there is no control group to compare it to. The unfalsifiability problem keeps the signal from reaching the people who most need it.&lt;/p&gt;

&lt;p&gt;Microservices, event-driven architecture, NoSQL databases — each originated as a genuine response to a genuine problem at genuine scale. Each got adopted as a default by teams who never encountered the scale problem the pattern was designed to solve, chosen not for their engineering properties but for their cultural visibility. And each, in its own way, does the same thing: it relocates the signal.&lt;/p&gt;

&lt;p&gt;The code stops complaining. The architecture absorbs the contradiction without surfacing it. The problem does not go away — it moves to the production data, two years out, in a form that is harder to find, harder to trace, and harder to fix than the loud failure it replaced.&lt;/p&gt;

&lt;p&gt;They are not solutions to the underlying problem. They are ways of making the underlying problem less observable. Which, at sufficient scale, is worse.&lt;/p&gt;




&lt;h2&gt;
  
  
  Engineering for Properties, Not Popularity
&lt;/h2&gt;

&lt;p&gt;The correction is not a methodology. It cannot be certified. It is a discipline of asking a question that the industry has largely stopped asking: what properties does this technology choice provide, and what properties does it remove?&lt;/p&gt;

&lt;p&gt;A relational database provides transactional consistency, referential integrity, and constraint enforcement as structural guarantees — not as features to be implemented, but as properties of the system that exist regardless of what any individual piece of code does. Removing it in favour of a document store or a distributed data layer removes those guarantees. They do not vanish. They become engineering problems to be solved in the application, maintained indefinitely, by teams who may not fully understand why they are there.&lt;/p&gt;

&lt;p&gt;A single deployable unit with a coherent consistency boundary provides loud failure for free. Splitting that unit across services and event queues removes it. Sagas and compensation logic can partially recover it — for the scenarios that were anticipated. For the ones that weren't, which is the only class of scenario this article has been about, recovery depends on logic that by definition could not have been written yet.&lt;/p&gt;

&lt;p&gt;These are not preferences. They are structural choices with structural consequences, most of which arrive too late to be traced back to the decision that produced them.&lt;/p&gt;

&lt;p&gt;The organisation that chooses its database because a large tech company uses it, its architecture because it was the subject of last year's conference circuit, its framework because it is what the available developers already know — is not making engineering decisions. It is making fashion decisions and calling them engineering. The difference between the two is not visible on the day the choice is made. It is visible in the production data, two years later, when the unconsidered scenario finally arrived and the system had no mechanism to surface it.&lt;/p&gt;

&lt;p&gt;A coherent contextual center — a domain model that keeps what belongs together in one place, behind a single consistency boundary, enforced by the database that was built to enforce such things — is the structural embodiment of the principle this article has been arguing for. Not because it is elegant. Because it keeps failure loud, keeps the feedback loop intact, and keeps the system capable of correcting itself as reality reveals what nobody knew upfront.&lt;/p&gt;

&lt;p&gt;Which it will. It always does.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to Do With This
&lt;/h2&gt;

&lt;p&gt;If you are building a small application with a bounded scope and a limited lifespan, the considerations above matter less. The unconsidered scenario may never arrive. The combination space never gets large enough. Disposable software benefits from disposable development, and the current generation of tools makes disposable development faster than it has ever been.&lt;/p&gt;

&lt;p&gt;If you are building something large, long-running, and business-critical — the kind of system that needs to remain correct through changing requirements and changing teams over years — the question worth asking before framework selection, before architecture diagrams, before any technology choice is made, is this: what is the failure mode of this decision, and when will it announce itself?&lt;/p&gt;

&lt;p&gt;Choose technologies for what they provide, not for what they are associated with. Prefer the ones that keep failure loud and immediate over the ones that keep it quiet and deferred. Accept that no amount of upfront specification eliminates the unconsidered scenario — and build systems that surface it fast, correct it cheaply, and carry the correction forward in a form that survives the next team.&lt;/p&gt;

&lt;p&gt;The loudest signal that this discipline is absent will not come from the code. It will come from the data, later than you expect, in a form that is harder to explain than a bug and more expensive to fix than a refactor.&lt;/p&gt;

&lt;p&gt;The best time to build for loud failure was at the start of the project. The second best time is before the data starts lying to you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is part of a series on software engineering craft. Other pieces examine the rich domain model as a discipline, the properties of enterprise software that lasts, and why the practices that prevent structural decay are the same ones that always prevented it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwaredevelopment</category>
      <category>java</category>
      <category>architecture</category>
    </item>
    <item>
      <title>SOLID Principles: Forks to Eat Soup</title>
      <dc:creator>Leon Pennings</dc:creator>
      <pubDate>Tue, 23 Jun 2026 07:46:54 +0000</pubDate>
      <link>https://dev.to/leonpennings/solid-principles-forks-to-eat-soup-1mgm</link>
      <guid>https://dev.to/leonpennings/solid-principles-forks-to-eat-soup-1mgm</guid>
      <description>&lt;h2&gt;
  
  
  The Problem With Architecture Debates
&lt;/h2&gt;

&lt;p&gt;Most software architecture debates can't actually be settled. Every system is built once. The alternative approach — the one that wasn't chosen — is never built alongside it, under the same conditions, with the same team, against the same market. So when a system works, "it works" gets quietly promoted to "the approach was right," and when a system rots, the rot gets blamed on the domain being inherently complex, or the requirements changing too much, or the previous developers having been careless. Almost never does anyone conclude that the architecture itself was the variable that mattered, because there is no control group to compare it to.&lt;/p&gt;

&lt;p&gt;This is the unfalsifiability problem, and it is the reason architecture discussions tend to be so unproductive. Everyone is generalizing from an n of one, or a handful of isolated ones, with team skill, domain difficulty, and plain luck as uncontrolled variables throughout. Two competent engineers can each have ten years of experience, complete confidence in their conclusions, and have learned nothing transferable to each other, because neither has ever seen their belief tested against an alternative.&lt;/p&gt;

&lt;p&gt;It is also the reason a particular class of mistake can persist for decades, spread through teams and codebases, get taught in courses and validated in job interviews, and still never be clearly identified as a mistake. The code ships. The system works well enough. The costs are real but diffuse — spread across maintenance cycles, onboarding friction, debugging sessions that take longer than they should. Nobody writes a post-mortem that says "we had too many interfaces." They write one that says "the codebase had become difficult to change," and then someone suggests that what happens is the nature of enterprise software.&lt;/p&gt;

&lt;p&gt;It is easy to use the wrong tool for the job. The wrong tool could even lead to a worse outcome than the problem it was intended to solve. SOLID principles are design principles for an object-oriented domain model. Applying them outside one is every bit as effective as eating soup with a fork.&lt;/p&gt;




&lt;h2&gt;
  
  
  Forks to Eat Soup
&lt;/h2&gt;

&lt;p&gt;Nobody uses a fork to eat soup. You could. With enough patience and a sufficiently shallow bowl, you would eventually get most of it. If you had only ever eaten soup with a fork, you might genuinely believe that forks are general-purpose eating tools, and that the difficulty of soup simply reflects the nature of liquids rather than the inadequacy of the instrument.&lt;/p&gt;

&lt;p&gt;SOLID principles have a specific, valid purpose: structuring a rich object-oriented domain model. Applied there, they produce code that is coherent, maintainable, and expressive. Applied elsewhere — to procedural code, to technical infrastructure, to layered frameworks with no meaningful domain model underneath — they tend to produce indirection without purpose. The complexity that follows gets blamed on the problem domain. The instrument is rarely questioned.&lt;/p&gt;

&lt;p&gt;When the domain modelling step is skipped, or reduced to a handful of JPA-annotated data transfer objects that carry field values but no behavior, the problems start. What remains is procedural code and technical plumbing. SOLID principles, taught as universal good practice in object-oriented application, get applied to the codebase that remains — the one where the domain model should have been. The result is the software equivalent of eating soup with a fork: technically possible, enormously effortful, and solving a problem that the right tool would not have created.&lt;/p&gt;

&lt;p&gt;To understand why, it helps to look at what each principle actually means — and what it tends to mean in practice instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Single Responsibility Principle
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;A class should have only one reason to change.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The intent is to prevent fat objects — objects that accumulate unrelated responsibilities until they become incomprehensible. A &lt;code&gt;Customer&lt;/code&gt; object should be responsible for what a customer &lt;em&gt;is and does&lt;/em&gt; in the domain. It should not also be responsible for rendering HTML, managing database transactions, and sending emails. Those are different concerns, and mixing them means changes to any one of them risk breaking all the others.&lt;/p&gt;

&lt;p&gt;At the modelling level this is a sound and useful principle. The difficulty arises when it gets applied at the code level, where it tends to appear in two distinct patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 1: Technology Layering as Responsibility Separation
&lt;/h3&gt;

&lt;p&gt;A common pattern treats technology boundaries as responsibility boundaries. A &lt;code&gt;Customer&lt;/code&gt; domain object gets split into &lt;code&gt;CustomerDTO&lt;/code&gt;, &lt;code&gt;CustomerRepository&lt;/code&gt;, &lt;code&gt;CustomerService&lt;/code&gt;, and &lt;code&gt;CustomerController&lt;/code&gt; — one object per architectural layer. The reasoning tends to follow SRP vocabulary: "the domain object shouldn't know about persistence," "the service layer shouldn't know about HTTP."&lt;/p&gt;

&lt;p&gt;What this produces, though, is not responsibility separation. It is technology separation. The responsibility of understanding what a customer is and does in the domain has not been clarified — it has been distributed across four classes that must all be read together to recover the meaning that one coherent object would have expressed directly.&lt;/p&gt;

&lt;p&gt;The cost is not visible at the time of writing. The developer who built it holds all the context in their head. The cost materializes six months later, when something needs changing, or when a new developer joins the team.&lt;/p&gt;

&lt;p&gt;Context is what keeps software maintainable. Distributing it across layers replaces semantic meaning with structural convention. The cognitive load of every subsequent change increases, because understanding what the code does requires mentally reassembling the essential logic from across an accidental structure.&lt;/p&gt;

&lt;p&gt;Technology boundaries are not responsibility boundaries. A domain object can contain a database query if that is what its responsibility requires. It can make an HTTP call. Consider &lt;code&gt;email.send()&lt;/code&gt; — not a static utility method, but a method on an existing email instance, because sending is part of what an email &lt;em&gt;is&lt;/em&gt;. The SMTP detail is essential complexity: it exists because reality requires it. The &lt;code&gt;EmailService&lt;/code&gt;, &lt;code&gt;IEmailSender&lt;/code&gt;, &lt;code&gt;SmtpEmailSender&lt;/code&gt; stack that replaces it is accidental complexity: it exists because architectural doctrine requires it. The Email concept is the only place in the entire application where sending belongs, and it is exactly where a reader would expect to find it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 2: Dual Responsibility Objects in DDD Contexts
&lt;/h3&gt;

&lt;p&gt;Domain-Driven Design introduces bounded contexts — the idea that the same real-world entity may appear differently in different parts of the system. A customer in a shipping context has different relevant attributes and behaviors than a customer in a billing context. This is correct and useful modelling.&lt;/p&gt;

&lt;p&gt;One reading of this is to create multiple &lt;code&gt;Customer&lt;/code&gt; variants — a &lt;code&gt;ShippingCustomer&lt;/code&gt;, a &lt;code&gt;BillingCustomer&lt;/code&gt; — each carrying a subset of customer responsibility plus their context-specific concerns mixed together. The intent is usually good: keep each object focused, avoid a single bloated &lt;code&gt;Customer&lt;/code&gt; that knows too much. But what tends to happen is that customer responsibility ends up duplicated and fragmented across multiple objects, none of which is a coherent model of what a customer actually is. The object has not been given a single responsibility — it has been given two, then renamed to disguise the second one.&lt;/p&gt;

&lt;p&gt;A more faithful reading is composition rather than decomposition. &lt;code&gt;Customer&lt;/code&gt; retains its single responsibility: being a customer. Shipping-specific behavior belongs in a separate object that &lt;em&gt;has&lt;/em&gt; a Customer as an attribute and adds its own behavior alongside it — not a Customer subtype, but an add-on. Consider the difference in how objects are constructed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order order = new Order(customer, shoppingCart);
Payment payment = new Payment(invoice);
Email email = new Email(subject, content);
email.send();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each object owns exactly what its concept requires. &lt;code&gt;Customer&lt;/code&gt; does not know about &lt;code&gt;Order&lt;/code&gt;. &lt;code&gt;Order&lt;/code&gt; knows about &lt;code&gt;Customer&lt;/code&gt; because an order belongs to someone — that relationship is explicit in the construction, not smuggled in through inheritance or layer-crossing services. The domain model is the spoon. The principles apply cleanly when there is something to apply them to.&lt;/p&gt;

&lt;p&gt;What bounded contexts produce, when modelled this way, is not multiple versions of the same object — it is context-specific objects that &lt;em&gt;use&lt;/em&gt; domain objects without absorbing them. The core object stays coherent because it is never asked to be something it isn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  Open/Closed Principle
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Software entities should be open for extension but closed for modification.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The principle addresses inheritance. A well-designed superclass establishes invariants and behavior that subclasses can extend — adding new behavior — without modifying what the superclass already guarantees. The superclass contract remains stable. Subclasses enrich it.&lt;/p&gt;

&lt;p&gt;The classic illustration of where this gets complicated is the circle and ellipse problem. Should &lt;code&gt;Circle&lt;/code&gt; extend &lt;code&gt;Ellipse&lt;/code&gt;, or &lt;code&gt;Ellipse&lt;/code&gt; extend &lt;code&gt;Circle&lt;/code&gt;? The intuition that one is a special case of the other seems to invite inheritance. But the behavioral contracts pull in different directions: a circle maintains the invariant that all radii are equal, which an ellipse cannot guarantee. Inheriting one from the other tends to force the subclass to either suppress an inherited invariant or override inherited behavior — which is precisely the situation OCP is designed to avoid. The more natural model is that both are shapes. What they share belongs in a common abstraction. What makes them distinct belongs in separate implementations.&lt;/p&gt;

&lt;p&gt;A constructor exists to bring an object to a correct, fully initialized state — to establish its invariants before anything else acts on it. A subclass that introduces behavior dependent on superclass state before that initialization is complete is extending something that does not yet fully exist. When this pattern becomes necessary, it is often worth asking whether the inheritance relationship itself is the right one. The language permitting something does not make it structurally sound.&lt;/p&gt;




&lt;h2&gt;
  
  
  Liskov Substitution Principle
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Objects of a subclass should be substitutable for objects of the superclass without altering the correctness of the program.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Liskov Substitution follows naturally from Open/Closed. If a subclass only extends the superclass — adding behavior without overriding or suppressing it — then substitutability tends to follow. The situations where LSP breaks down are usually the same situations where OCP has already broken down: a subclass that modifies rather than extends.&lt;/p&gt;

&lt;p&gt;The substitutability question is not about whether two things look similar in some states. An empty bank account and a non-existent bank account may show the same balance. They are not substitutable — one has an owner, a history, a legal existence, and obligations that the other does not. A broken watch displays the correct time twice each day, by coincidence rather than function. Apparent equivalence in certain states is not a subtype relationship. The contract needs to hold across all states and all behaviors, not just the ones that happen to align at a given moment.&lt;/p&gt;

&lt;p&gt;The practical guide to avoiding these violations is simpler than it might appear. Inheritance is the right relationship when something genuinely &lt;em&gt;is a&lt;/em&gt; more specific version of something else — when the full behavioral contract of the parent holds for the child without suppression or override. Composition is the right relationship when something &lt;em&gt;has a&lt;/em&gt; reference to something else and adds its own behavior alongside it. The domain tells you which one applies.&lt;/p&gt;

&lt;p&gt;A useful test is to say the relationship out loud. "A ShippingCustomer &lt;em&gt;is a&lt;/em&gt; Customer" — but is it? A ShippingCustomer is not a more specific kind of Customer. It is shipping-related information that belongs &lt;em&gt;to&lt;/em&gt; a Customer. Saying it out loud already suggests the right structure: it &lt;em&gt;has a&lt;/em&gt; Customer, it is not one. Inheritance chosen for convenience rather than conceptual accuracy is where both OCP and LSP tend to break down — not because the principles are hard to understand, but because the modelling question was never asked in the first place.&lt;/p&gt;




&lt;h2&gt;
  
  
  Interface Segregation Principle
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Clients should not be forced to depend on interfaces they do not use.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;ISP is to interfaces what SRP is to objects. A fat interface — one that bundles more methods than any single client will ever use — puts implementors in an awkward position. Every class that implements it must account for methods it has no use for, either by leaving them empty, throwing exceptions, or providing stub implementations that do nothing useful. The interface has become a convenience bundle rather than a coherent behavioral contract.&lt;/p&gt;

&lt;p&gt;The Java &lt;code&gt;Servlet&lt;/code&gt; interface is a well-known example of this. It defines methods for handling every HTTP verb, managing initialization and destruction, accessing configuration, and more. A developer implementing a simple endpoint must engage with the full surface of this interface regardless of how little of it they need. The interface grew to represent everything that might ever be useful to something servlet-like, rather than any single coherent concept. When an interface has accumulated that many methods, it is usually worth asking whether it is modelling one thing or several — and whether splitting it into focused contracts might serve implementors better.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dependency Inversion Principle
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;DIP is arguably the most consequential of the five principles in terms of how it has shaped contemporary codebases — and also the one where the distance between the original intent and common practice is widest.&lt;/p&gt;

&lt;p&gt;The principle addresses conceptual dependencies. In a rich domain model, a high-level concept should not be coupled to the concrete technical mechanism that implements a lower-level concern. The dependency should point toward an abstraction that is meaningful in the domain — one that a domain expert would recognize — rather than toward a specific technical artifact.&lt;/p&gt;

&lt;p&gt;This is a modelling principle. It says that domain concepts should be defined in terms of what they need behaviorally, not in terms of the specific technology that happens to fulfill that need today.&lt;/p&gt;

&lt;p&gt;In contemporary practice, DIP is frequently invoked to justify dependency injection — the pattern of passing dependencies into a class from the outside rather than having the class create them directly. DI frameworks have made this the architectural default: everything should be injectable, which means everything needs an interface, which means interface creation becomes a routine act disconnected from any modelling decision.&lt;/p&gt;

&lt;p&gt;The reasoning tends to be circular. DI is presented as an application of DIP. But the abstraction that DI creates — a production datasource and a mock datasource sitting behind a common interface — is not necessarily a domain concept. It may exist primarily because the test harness requires something swappable, and the production codebase has been structured to accommodate that requirement.&lt;/p&gt;

&lt;p&gt;This points to a tension that is worth naming directly: a codebase optimized for production and a codebase optimized for testing tend to pull in different directions. A production-optimized codebase is direct and expressive. The model is clear. The surface area of failure is small. Fewer tests are needed because the code is understandable and the concepts are coherent. A test-optimized codebase introduces interfaces, injection seams, and mock implementations. The production behavior becomes something inferred through a scaffolding of substitutes. More tests tend to be needed partly because the indirection introduced for testing creates new failure modes that themselves require coverage.&lt;/p&gt;

&lt;p&gt;There is a certain irony in this: a test-optimized codebase can end up requiring more tests to manage the complexity that was introduced in order to make testing easier.&lt;/p&gt;

&lt;p&gt;The analogy to aspect-oriented programming is worth considering here. AOP was introduced as a way to separate concerns. The practical difficulty was that behavior was happening in the codebase that could not be found by reading the code. Large dependency injection containers can have the same quality. The actual wiring of the application is not directly readable — it is inferred from annotations and container configuration at runtime. Debugging means navigating proxy layers and generated code rather than following what is explicitly written. The indirection that was meant to simplify things for the original author can make things considerably harder for everyone who comes after.&lt;/p&gt;

&lt;p&gt;The question DIP is actually asking at the modelling level is different: does this dependency reflect a real conceptual relationship, or is it an artifact of how the code happens to be built? If a domain concept depends on a behavioral abstraction that belongs in the domain — something a domain expert would name and recognize — then DIP is doing its intended work. If the abstraction exists only because a framework requires it, or because a test needs something swappable, then DIP's vocabulary is being borrowed to justify an infrastructure decision. The principle has not been applied. It has been appropriated.&lt;/p&gt;




&lt;h2&gt;
  
  
  What The Principles Actually Share
&lt;/h2&gt;

&lt;p&gt;Looking across all five, the same underlying concern appears in each one: conceptual integrity at the model level.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SRP&lt;/strong&gt;: one coherent concept per object&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OCP&lt;/strong&gt;: inheritance respects the concept's contract&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;LSP&lt;/strong&gt;: substitutability follows from genuine subtype relationships&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ISP&lt;/strong&gt;: interfaces reflect actual behavioral contracts, not convenience bundles&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DIP&lt;/strong&gt;: dependencies follow conceptual relationships, not technical ones&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When these principles are applied to a rich domain model, they reinforce each other. When they are applied to layers, frameworks, and testing infrastructure — in the absence of a model — they tend to produce fragmentation instead of coherence, indirection instead of clarity, and more complexity in the name of managing complexity.&lt;/p&gt;

&lt;p&gt;The principles were designed for a specific activity: building a rich object-oriented model of a domain. When that activity is skipped — when the model is reduced to annotated data containers and the real work happens in procedural service classes — the principles have no natural target. What gets built in their name may look structured, but the structure serves the principles rather than the domain. The soup gets eaten. It just takes considerably longer than it should.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Practical Case for Getting This Right
&lt;/h2&gt;

&lt;p&gt;A rich domain model and the principles that support it can look like a theoretical exercise — an indulgence for architects with time to spare, impractical against real deadlines. This perception is worth examining because the costs and benefits are often misunderstood.&lt;/p&gt;

&lt;p&gt;A domain model does not require weeks of upfront design. Even a first pass — an hour or two of thinking through what the concepts are, what they own, what they do — produces something to code from. Not instructions to implement, but domain knowledge to express. That difference in orientation matters more than it might sound. Procedural development requires scaffolding: repositories to call, services to extend, state to pass between layers, behavior to locate across a structure that exists for technical rather than conceptual reasons. A domain model reduces that scaffolding substantially. The concepts know what they are. The behavior lives where it belongs. Adding a capability often means adding a method to an existing object rather than extending a service, adding a repository call, and wiring the result through a chain of layers.&lt;/p&gt;

&lt;p&gt;The total amount of code a rich domain model requires is less — often considerably less — than its procedural equivalent. This is not a long-term payoff that arrives after years of maintenance. It is present from the beginning, in the clarity of direction the model provides and the scaffolding it makes unnecessary.&lt;/p&gt;

&lt;p&gt;The maintenance advantage compounds from there. Procedural systems with fat services and an anemic model distribute context across layers. Each change requires reassembling that context. Each new developer inherits a cognitive load that grows with the codebase. The tests added to manage the complexity require maintenance of their own. A rich domain model stays navigable because the concepts remain coherent and the code remains an expression of them.&lt;/p&gt;

&lt;p&gt;The other thing that happens with practice is harder to quantify but worth naming. Thinking in responsibilities — asking what a concept &lt;em&gt;is&lt;/em&gt;, what it &lt;em&gt;does&lt;/em&gt;, what it should &lt;em&gt;know&lt;/em&gt; — becomes second nature. The modelling step stops feeling like overhead and starts feeling like the work itself. Code becomes an expression of that model rather than a template-filling exercise. The principles stop being rules to apply and start being observations about whether the model is coherent.&lt;/p&gt;

&lt;p&gt;That is what SOLID principles are for. Not for services, not for layers, not for test infrastructure. For the model. And a good model, it turns out, is not a theoretical luxury. It is the most practical thing a codebase can have.&lt;/p&gt;

&lt;p&gt;Forks are excellent tools. So are SOLID principles. The question worth asking is not whether you are applying them, but whether you are applying them to something they were designed for.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>solidprinciples</category>
      <category>java</category>
      <category>oop</category>
    </item>
    <item>
      <title>What is the reason for using a rich domain model in the age of AI?</title>
      <dc:creator>Leon Pennings</dc:creator>
      <pubDate>Fri, 19 Jun 2026 11:39:56 +0000</pubDate>
      <link>https://dev.to/leonpennings/what-is-the-reason-for-using-a-rich-domain-model-in-the-age-of-ai-3gg</link>
      <guid>https://dev.to/leonpennings/what-is-the-reason-for-using-a-rich-domain-model-in-the-age-of-ai-3gg</guid>
      <description>&lt;p&gt;Most software architecture debates can't actually be settled. Every system is built once. The alternative approach — the one that wasn't chosen — is never built alongside it, under the same conditions, with the same team, against the same market. So when a system works, "it works" gets quietly promoted to "the approach was right," and when a system rots, the rot gets blamed on the domain being inherently complex, or the requirements changing too much, or the previous developers having been careless. Almost never does anyone conclude that the architecture itself was the variable that mattered, because there is no control group to compare it to.&lt;/p&gt;

&lt;p&gt;This is the unfalsifiability problem, and it is the reason architecture discussions tend to be so unproductive. Everyone is generalizing from an n of one, or a handful of isolated ones, with team skill, domain difficulty, and plain luck as uncontrolled variables throughout. Two competent engineers can each have ten years of experience, complete confidence in their conclusions, and have learned nothing transferable to each other, because neither has ever seen their belief tested against an alternative.&lt;/p&gt;

&lt;p&gt;If we can't run the controlled experiment, we need a substitute. Fred Brooks gave us most of one, decades ago, by separating essential complexity — the difficulty that comes from what the problem actually is — from accidental complexity, the difficulty we introduce ourselves through our tools, our process, our representations. Brooks' point was that a lot of suffering in software is self-inflicted, layered on top of a problem that wasn't that hard to begin with.&lt;/p&gt;

&lt;p&gt;What Brooks didn't give us is an operational test — a question you can ask in the middle of an actual design decision to tell which kind of complexity you're looking at. That's the test this article is trying to supply: was this decision forced by a genuine, current understanding of the domain, or was it forced by a constraint that existed before that understanding did? Essential complexity should always be the thing leading. Accidental complexity should always be downstream of it, serving it. The moment that order inverts — the moment a technology choice, a deployment topology, or a process gate starts dictating what the domain is allowed to look like — you have accidental complexity in charge, and the system will eventually make you pay for it.&lt;/p&gt;

&lt;p&gt;This problem is more urgent now than it has been at any point before, for a reason this article will come back to at the end: AI has made implementation — the actual writing of code — nearly free. Free implementation removes exactly the kind of friction that used to nudge developers toward a correct model almost by accident, whether or not they could ever have named what they were doing. What's left, once that friction is gone, is only the question of whether anyone is still asking it on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tool, and what it takes for the tool to work
&lt;/h2&gt;

&lt;p&gt;A rich domain model is, I'll argue, a tool — not a style preference, not an aesthetic about classes versus functions, but a tool built for three specific jobs. It's how you learn what a domain actually is, since the act of trying to give a concept a clean shape is what exposes whether you understood it in the first place. It's how you define a domain precisely enough that "what to build" stops being a matter of taste or memory. And it's how you document a domain in a form that has to keep working — unlike a diagram or a wiki page, which can drift quietly out of date for years with nobody noticing, a domain model that's wrong tends to say so. It is essential complexity made tangible — something you can actually point at — and testable — something that tells you when it's wrong, rather than something you have to take on faith.&lt;/p&gt;

&lt;p&gt;That's the claim the rest of this article is going to spend its length defending. It comes with a condition attached, because a tool only does its job under specific circumstances, and most of the software industry's familiar habits — fat service layers, splitting early into bounded contexts or microservices, treating a new user story as a work order instead of as evidence — violate that condition constantly, usually without anyone noticing they've done it.&lt;/p&gt;

&lt;p&gt;The condition has three parts. Essential complexity has to stay whole — in one place, reachable by one mind at a time, not dispersed across two hundred services inside a single codebase, and not dispersed across a boundary drawn between teams or deployments. The model has to give feedback when your understanding of it turns out to be incomplete — a behavior with no natural home, a compile error at every site that assumed an old shape, a constraint violated at the exact moment an assumption proves wrong. And folding new insight into the model, once you have it, has to stay cheap — paid once, in one place, rather than hunted for across however many places happened to encode the old understanding. Lose any one of these three and the tool stops being a tool. The code may still run. There may even be a model on a slide somewhere. But the thing that was supposed to be doing this work isn't doing it anymore — only its appearance survives.&lt;/p&gt;

&lt;p&gt;Take the first condition first, because it's the one most quietly violated, usually with the best of intentions. When essential complexity lives inside one core model, you can look at the model and see the business. When it doesn't — when it's spread across fat services, buried in repositories, scattered across distributed components, or split along departmental lines that felt obvious at the time — the legibility is the first casualty, and with it goes the ability to even ask the question this article opened with: is the application serving the domain, or has the domain quietly started serving the accidental complexity that was supposed to be in service of it? Once essential complexity stops having one visible, coherent home, that observation can no longer be made by anyone, because there's no longer a single place left to look. What follows is, in effect, an extended demonstration of what it costs to lose each of these three conditions, one at a time — and of how rarely losing them announces itself as a mistake while it's happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  A model is something you learn through, not something you draw once
&lt;/h2&gt;

&lt;p&gt;Take a deliberately simple example: a library that lends things out. Old and familiar on purpose, so the reasoning is the point, not the subject matter.&lt;/p&gt;

&lt;p&gt;The first conversation with the domain expert goes predictably. The library wants to lend books. They want to know where each book is — on a shelf, or on loan to someone, from when until when.&lt;/p&gt;

&lt;p&gt;The path of least resistance puts the loan dates directly on &lt;code&gt;Book&lt;/code&gt;. The book knows where it is; if it's out, it knows to whom and until when. It seems natural enough that most developers wouldn't pause on it.&lt;/p&gt;

&lt;p&gt;But pause on it anyway, because this is the decision that quietly constrains everything downstream of it. Ask a plain domain question: is knowing when a book was borrowed, and by whom, part of what a book &lt;em&gt;is&lt;/em&gt;? A book is a title, an author, a physical object. A loan is an event — an agreement between the library and a person, at a point in time, about that book. These are different things, stitched together for convenience, the same category of error as storing someone's employment history inside their passport.&lt;/p&gt;

&lt;p&gt;There's a structural problem hiding behind the conceptual one, too. A book gets borrowed many times, by different people, at different points in time. A single set of loan fields on &lt;code&gt;Book&lt;/code&gt; can't represent that history without overwriting it on every new loan. This isn't a style complaint — the model is structurally incapable of answering questions the business will eventually ask.&lt;/p&gt;

&lt;p&gt;So a &lt;code&gt;Loan&lt;/code&gt; entity gets introduced. It points to a book and a borrower, and carries its own data: start date, end date, return date. &lt;code&gt;Book&lt;/code&gt; goes back to being just a book. Each concept is responsible for what it actually is.&lt;/p&gt;

&lt;p&gt;Nobody asked for this refinement. The user story was "we want to lend out books," not "please separate the concept of a loan from the concept of a book." But the story was never a specification — it was a piece of information about the domain, and the job was to ask what it revealed, not to type it directly into a &lt;code&gt;Book&lt;/code&gt; class and close the ticket.&lt;/p&gt;

&lt;p&gt;Once &lt;code&gt;Loan&lt;/code&gt; exists as its own thing, something becomes visible that nobody requested: how many times a book has been borrowed this year, whether it's going out back-to-back often enough to justify a second copy, which loans are overdue right now, which borrower has the most items out. None of this required touching the model again. It fell out of having put the responsibility in the right place the first time. A correct abstraction doesn't just solve the stated problem — it stops resisting the next ten questions nobody has asked yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  The second correction
&lt;/h3&gt;

&lt;p&gt;A new requirement arrives: the library wants to lend DVDs too.&lt;/p&gt;

&lt;p&gt;The path of least resistance here is just as easy to predict: add a &lt;code&gt;DVD&lt;/code&gt; entity. Title, director, runtime. Close the ticket. And this is exactly the failure this whole article is about, in miniature — the request "we also want to lend DVDs" got treated as an instruction to add a &lt;code&gt;DVD&lt;/code&gt; class, instead of as new information about a domain that had just revealed something about itself.&lt;/p&gt;

&lt;p&gt;The actual question isn't "how do we add DVD." It's: was &lt;code&gt;Book&lt;/code&gt; ever the right concept for this domain in the first place? The lending system doesn't care that a book has pages or a DVD has a runtime. It cares that both are things that can be borrowed, tracked, and returned. Model &lt;code&gt;Book&lt;/code&gt; and &lt;code&gt;DVD&lt;/code&gt; as siblings and the next story brings magazines, then tools, then something that breaks the pattern outright, and four parallel entity types are now duplicating service logic and complicating every report.&lt;/p&gt;

&lt;p&gt;The concept the domain actually needed, it turns out, was never &lt;code&gt;Book&lt;/code&gt;. It was &lt;code&gt;LendableItem&lt;/code&gt; — something that can be lent, regardless of what it physically is. &lt;code&gt;Book&lt;/code&gt; becomes &lt;code&gt;LendableItem&lt;/code&gt;; what kind of item it is becomes data (&lt;code&gt;ItemType&lt;/code&gt;), not a class; the attributes specific to a type (ISBN and author for a book, runtime and director for a DVD) live in a small typed collection shaped by that &lt;code&gt;ItemType&lt;/code&gt;. A new lendable thing can be defined through configuration, without a release.&lt;/p&gt;

&lt;p&gt;This isn't abstraction for its own sake — starting with &lt;code&gt;Book&lt;/code&gt; was the right call when only books existed; naming a concept after its only known instance is reasonable, not naive. The point is that when the second instance arrived, it was &lt;em&gt;evidence&lt;/em&gt;, and the model was obligated to respond to evidence rather than absorb it as a special case bolted onto the side of the original guess.&lt;/p&gt;

&lt;p&gt;Here is the part worth sitting with: &lt;strong&gt;in both corrections, the cost of being wrong was paid exactly once, at exactly one place, and the compiler told you everywhere else that needed to change.&lt;/strong&gt; Turning &lt;code&gt;Book&lt;/code&gt; into &lt;code&gt;LendableItem&lt;/code&gt; produces a wave of compile errors at every call site that assumed a &lt;code&gt;Book&lt;/code&gt; — every one of them a worked checklist, not a hunt. There is no step where you have to remember which of fourteen services touched the old assumption. The type system already knows.&lt;/p&gt;

&lt;p&gt;Picture the alternative: a codebase with two hundred service methods, accumulated over years, several of them written by people who've since left. Some of those services read a book's loan status off a flag on &lt;code&gt;Book&lt;/code&gt;. Some duplicate the "is this thing currently out" check inline. Some call into a shared &lt;code&gt;BookService&lt;/code&gt; that does it correctly, and some call into an older one that doesn't quite. When the DVD requirement lands, &lt;em&gt;finding&lt;/em&gt; every place that encoded an assumption about books is now a research project, conducted from memory and grep, with no tool confirming you found all of them — and if two different developers wrote two of those services, they may have encoded two subtly different mental models of what a book even is, neither of which was ever forced to reconcile with the other, because nothing in the architecture ever made them collide.&lt;/p&gt;

&lt;p&gt;That's two of this article's three conditions doing their work at once: the model gave feedback the moment a concept had no natural home to be wrong in, and folding that correction back in cost one change, enforced by a tool, rather than a hunt across however many places had quietly encoded the old assumption. That's the actual argument for a domain model, stated as plainly as I can: it is the cheapest known way to be wrong, because being wrong gets caught in one place, by a tool, instead of being wrong silently in fourteen places, caught eventually by a domain expert noticing the software does something they never agreed to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fat is not a size problem
&lt;/h2&gt;

&lt;p&gt;The library example shows feedback and cheap correction working together, inside a single concept. The first condition — that essential complexity stays whole — fails differently, and far more commonly, and it's worth seeing exactly how, because the failure is almost always mistaken for a different problem than it is.&lt;/p&gt;

&lt;p&gt;Take &lt;code&gt;Customer&lt;/code&gt;. Almost every enterprise system has one, and almost every one of them eventually starts absorbing things that don't belong to it: a &lt;code&gt;preferredCarrier&lt;/code&gt; field set because shipping needed it, a &lt;code&gt;creditLimit&lt;/code&gt; because billing needed it, an &lt;code&gt;slaTier&lt;/code&gt; because support needed it. Years of this, and &lt;code&gt;Customer&lt;/code&gt; is enormous — hundreds of fields, half of them nullable, conditional logic scattered through anything that touches it, and nobody able to describe what &lt;code&gt;Customer&lt;/code&gt; actually means anymore, because it means five different things depending on who's asking.&lt;/p&gt;

&lt;p&gt;This is a real failure, and the diagnosis matters, because two very different responses are available, and only one of them fixes anything.&lt;/p&gt;

&lt;p&gt;The popular response is to split. Give shipping its own &lt;code&gt;ShippingCustomer&lt;/code&gt;, billing its own &lt;code&gt;BillingCustomer&lt;/code&gt;, support its own &lt;code&gt;SupportCustomer&lt;/code&gt; — separate models, separate teams, separate services if you go all the way, joined by some kind of translation layer that maps one context's idea of a customer onto another's. This is the bounded-context move, and on paper it sounds disciplined: each context gets a clean, focused model instead of one bloated shared one.&lt;/p&gt;

&lt;p&gt;Look closer and notice what actually happened: &lt;code&gt;ShippingCustomer&lt;/code&gt; is not a different concept from the bloated &lt;code&gt;Customer&lt;/code&gt;. It's the same god object, just with the bloat partitioned by department instead of concentrated in one file. The information that crept into &lt;code&gt;Customer&lt;/code&gt; because nobody asked "whose responsibility is this" hasn't been resolved — it's been relocated, and the relocation comes with a new bill attached. Where before, a change to how loyalty tier affects shipping could be seen and verified in one place, by one compiler, it now has to travel: &lt;code&gt;Billing&lt;/code&gt;'s context has to publish something, &lt;code&gt;Shipping&lt;/code&gt;'s context has to subscribe to it, maintain its own copy, and recompute its own derived state asynchronously, hoping the event arrives, hoping the definitions of "loyalty tier" haven't quietly diverged between the two contexts that were specifically built not to share one. The coupling between billing and shipping didn't go away because they're now in different rooms. It just stopped being visible to anyone reading either room on its own — and a dependency you can't see is not a dependency you've solved, it's a dependency that will surface later, in production, as an "integration issue" nobody can trace back to its origin.&lt;/p&gt;

&lt;p&gt;This is the same shape as the god object, except distributed. Splitting the pain across contexts is, at best, splitting the pain — not preventing it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the fix actually looks like
&lt;/h3&gt;

&lt;p&gt;The right response to a fat &lt;code&gt;Customer&lt;/code&gt; is the same response that turned &lt;code&gt;Book&lt;/code&gt; into &lt;code&gt;Loan&lt;/code&gt; and &lt;code&gt;LendableItem&lt;/code&gt;: ask what responsibility doesn't belong here, and extract it — into a new, named concept, still inside the same model, still reachable by an ordinary reference, still subject to the same compiler.&lt;/p&gt;

&lt;p&gt;But extract it carefully, because there's a trap one level down that looks like a fix and isn't. The instinct might be to give &lt;code&gt;Customer&lt;/code&gt; a &lt;code&gt;List&amp;lt;ShippingPreference&amp;gt;&lt;/code&gt; directly — replace the flat &lt;code&gt;preferredCarrier&lt;/code&gt; field with a small polymorphic hierarchy of rules, ranked by precedence. That's progress over the flag, but it's still the same mistake in a thinner disguise: &lt;code&gt;Customer&lt;/code&gt; has no business knowing that shipping preferences exist as a concept at all. A &lt;code&gt;ShippingPreference&lt;/code&gt; living directly on &lt;code&gt;Customer&lt;/code&gt; is &lt;code&gt;Customer&lt;/code&gt; quietly absorbing knowledge of how it's consumed downstream — the exact failure that produced &lt;code&gt;ShippingCustomer&lt;/code&gt; in the first place, just wearing an interface instead of a flag.&lt;/p&gt;

&lt;p&gt;The responsibility that's missing a home isn't "the customer's shipping rules." It's "how this customer relates to shipping" — and that relationship is its own concept, with its own name: &lt;code&gt;CustomerShipping&lt;/code&gt;. It holds a reference to the &lt;code&gt;Customer&lt;/code&gt; it concerns, and a list of &lt;code&gt;CustomerShippingPreference&lt;/code&gt; instances — a default, a tier-based upgrade, an explicit override — each one only meaningful inside the context of shipping, which is exactly where they now live.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;CustomerShippingPreference&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;precedence&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;CarrierChoice&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DefaultShipping&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;CustomerShippingPreference&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;precedence&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;CarrierChoice&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CarrierChoice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"UPS"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofDays&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GoldTierShipping&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;CustomerShippingPreference&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;precedence&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;CarrierChoice&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CarrierChoice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DHL"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofDays&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExplicitDateOverride&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;CustomerShippingPreference&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;precedence&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;CarrierChoice&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CarrierChoice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestedCarrier&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestedDate&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomerShipping&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CustomerShippingPreference&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;preferences&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nc"&gt;CarrierChoice&lt;/span&gt; &lt;span class="nf"&gt;shippingMethodFor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;preferences&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Comparator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;comparingInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;CustomerShippingPreference:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;precedence&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Customer&lt;/code&gt; itself never branches on tier, never checks for an override, never holds a single field related to shipping — it doesn't even know &lt;code&gt;CustomerShipping&lt;/code&gt; exists. &lt;code&gt;Shipment&lt;/code&gt;, when it needs a carrier, doesn't ask &lt;code&gt;Customer&lt;/code&gt; anything directly. It takes an &lt;code&gt;Order&lt;/code&gt;, reads the &lt;code&gt;Customer&lt;/code&gt; off it, looks up or builds the &lt;code&gt;CustomerShipping&lt;/code&gt; for that customer, and asks &lt;em&gt;that&lt;/em&gt; object for the shipping method given the order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Shipment&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Shipment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;CustomerShippingRepository&lt;/span&gt; &lt;span class="n"&gt;shippingLookup&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;CustomerShipping&lt;/span&gt; &lt;span class="n"&gt;shipping&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shippingLookup&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forCustomer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;CarrierChoice&lt;/span&gt; &lt;span class="n"&gt;carrier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shipping&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;shippingMethodFor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A new rule — a holiday rush exception, a regional carrier restriction, a future platinum tier — is a new class implementing &lt;code&gt;CustomerShippingPreference&lt;/code&gt;, added to &lt;code&gt;CustomerShipping&lt;/code&gt;'s list, never touching &lt;code&gt;Customer&lt;/code&gt; at all. The arbitration logic — &lt;em&gt;given several applicable rules, which one wins&lt;/em&gt; — has exactly one home, and &lt;code&gt;Customer&lt;/code&gt; stays exactly as ignorant of shipping as &lt;code&gt;Book&lt;/code&gt; stayed ignorant of loans.&lt;/p&gt;

&lt;p&gt;This is a handful of small, plainly readable classes. It is not impressive-looking code. And it resolves more correctly, with less effort, than either the original flag-on-Customer design or the bounded-context split would have, because it correctly identifies what was actually going on twice over: not "customer is too big," but "shipping's view of a customer had no home, so it got jammed either into a field on &lt;code&gt;Customer&lt;/code&gt; or into a separate &lt;code&gt;ShippingCustomer&lt;/code&gt; clone — when what it actually needed was its own name, sitting between the two, owning exactly the relationship it represents and nothing else."&lt;/p&gt;

&lt;p&gt;Now try to build the same arbitration across three separate services — a shipping-preference service, a loyalty-tier service, an order-override service, however the bounded contexts happened to get drawn. The precedence rule doesn't belong to any one of them; it belongs to the relationship &lt;em&gt;between&lt;/em&gt; them — which is precisely the responsibility &lt;code&gt;CustomerShipping&lt;/code&gt; exists to hold — and that relationship has nowhere to live except in glue code outside all three contexts once it's been split that way: code nobody will consider part of "the domain," code that has to either make three synchronous calls and recompute the ranking itself, or maintain a denormalized, eventually-stale copy of all three rule types just to compare them locally. Either way, the actual essential complexity here — how privilege and explicit intent interact — has become homeless, in a system specifically designed to give every concept a clean home. Good luck.&lt;/p&gt;

&lt;p&gt;There's a second, less obvious benefit to &lt;code&gt;CustomerShipping&lt;/code&gt; worth naming, because it points at something larger than this one example. Notice what this design actually is: an add-on. It attaches a new concern to &lt;code&gt;Customer&lt;/code&gt; after the fact, without modifying &lt;code&gt;Customer&lt;/code&gt;, without &lt;code&gt;Customer&lt;/code&gt; ever being aware it exists. That's normally the property bounded contexts and microservices claim for themselves — loosely coupled, independently addable — except here it's achieved without any of the cost usually attached to it, because the looseness came from correct responsibility assignment, not from physical separation. It's glue, without the pain that usually comes with glue.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boundary as a bet you can't yet price
&lt;/h2&gt;

&lt;p&gt;Here is the order of moves so far, made explicit, because the second move only works after the first one has landed. First: most of what bounded contexts are reached for to fix — a bloated &lt;code&gt;Customer&lt;/code&gt;, a god object, departments fighting over one shared model — is solved more simply and more cheaply by keeping the first condition intact inside a single codebase: ask what responsibility doesn't belong, extract it into its own named object, connect it by reference. &lt;code&gt;CustomerShipping&lt;/code&gt; is the proof. The usual justification for splitting evaporates once the extraction is done properly, because the thing the split was trying to relieve never had to exist in the first place.&lt;/p&gt;

&lt;p&gt;Second, and this is the sharper claim: even where a boundary still looks justified on the day it's drawn — even if the team did genuine, careful event-storming, even if the language really does diverge between two parts of the business — the boundary is a bet, and it's a bet placed with incomplete information, because &lt;strong&gt;you cannot know today every cross-cutting rule the business will need tomorrow.&lt;/strong&gt; This is the same condition failing on a different axis. Inside a codebase, the failure mode was a name with no responsibility. Across a network, it's a boundary that looked justified on the day it was drawn, and wasn't, because the thing that would have falsified it hadn't happened yet. A boundary drawn between Customer-handling and Shipping-handling is implicitly a claim that nothing will ever need to act on both sides of that line atomically. That claim is being made before the business has finished telling you what it needs — and it never finishes, the same way the library's understanding of what a lendable thing was never finished after one conversation.&lt;/p&gt;

&lt;p&gt;The rule that eventually crosses the boundary doesn't have to be a compliance requirement. It's tempting to reach for GDPR's right to erasure as the example, because it's vivid and has a regulator attached — and it is a real instance of this, worth walking through on its own merits. A customer asks to be forgotten, and &lt;code&gt;Customer&lt;/code&gt; needs to be deleted, fully and verifiably. In a single database, behind a single transaction, this is mostly handled by the database itself: if &lt;code&gt;CustomerShipping&lt;/code&gt; references &lt;code&gt;Customer&lt;/code&gt; and nobody wrote code to remove it first, the foreign key constraint refuses the delete, loudly, immediately, pointing exactly at what's still attached — the same constraint that should also &lt;em&gt;prevent&lt;/em&gt; erasure when an order is still open or a complaint unresolved, again without anyone having to remember to write that check by hand. That failure is itself a small instance of the same learning loop the rest of this article has been describing: a &lt;code&gt;ConstraintViolationException&lt;/code&gt; at the moment of deletion is the system telling you, synchronously and for free, that your understanding of "what does removing a customer actually require" was incomplete — caught at the cheapest possible moment, before anything was lost. Spread &lt;code&gt;CustomerShipping&lt;/code&gt;'s data across an independently owned datastore in a separate service, and that guarantee disappears with it: there's no foreign key spanning two databases, so erasure becomes a saga of calls with compensating logic if any step fails, and the entire guarantee now depends on someone having remembered, months earlier, to wire &lt;code&gt;CustomerShipping&lt;/code&gt; into that flow. Forget one service and nothing breaks loudly. The data that should have been gone simply continues to exist, discovered eventually by an audit, if it's discovered at all.&lt;/p&gt;

&lt;p&gt;But making GDPR the centerpiece would be a mistake, because it hands every team without a regulator standing over them a clean exit: &lt;em&gt;we're not compliance-heavy, so this doesn't apply to us.&lt;/em&gt; It applies to them too, because the same shape of rule shows up constantly with no compliance angle at all. A loyalty program launches eighteen months in, and upgrading a customer mid-month needs to retroactively adjust the shipping terms on every order still in transit — Customer, Order, and &lt;code&gt;CustomerShipping&lt;/code&gt;, read and changed together. A fraud signal fires, and every open order and pending shipment for that customer needs to freeze atomically, in one step, not as three separate notifications hoping three separate systems all apply the freeze correctly and in time. An account gets closed, but anything already in transit is contractually entitled to still ship — one rule, reading across three concepts at once, treating each differently based on the others' current state. None of this is compliance. All of it is just Selling, understood a little more completely than it was on day one, the same way Loan and LendableItem were Lending, understood a little more completely than Book ever was.&lt;/p&gt;

&lt;p&gt;The price of having split early isn't paid on the day of the split. It's paid the day one of these rules arrives, and what would have been a few small domain objects — a class, a method, a foreign key — turns out instead to require an application integration effort: a saga, a compensating-transaction design, a new piece of cross-service observability just so anyone can tell, after the fact, whether the rule actually applied everywhere it needed to. That price was never on the table when the boundary was drawn, because the rule that triggers it didn't exist yet. The boundary wasn't wrong because the modeling was sloppy. It was wrong because it was a permanent commitment made against a domain that was still, and always will be, in the process of being discovered — and discovery doesn't pause for the convenience of an architecture diagram that's already been agreed on.&lt;/p&gt;

&lt;p&gt;There is a name for the assumption that a system can be correctly specified before the work of building it reveals what you didn't know. Waterfall made that assumption about requirements. Bounded contexts make the same assumption one level down — about domain boundaries. The parallel is precise: in both cases, a commitment is made at the moment of least knowledge, the commitment hardens as work accumulates on top of it, and the cost of the thing you didn't know becomes visible only after the commitment is too expensive to revise cheaply. The difference is that waterfall's failure eventually became undeniable enough that the industry moved on from it. The bounded-context version of the same mistake is currently being actively marketed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boundary that doesn't justify itself
&lt;/h2&gt;

&lt;p&gt;It's worth being explicit about why this keeps happening, because the architectural move — bounded contexts, services drawn along them — is usually defended with a real and legitimate-sounding observation: the same word genuinely means different things in different parts of a large business. A "policy" to an underwriter is not what a "policy" means to someone handling a claim. A "trade" looks different to the front office than to settlement.&lt;/p&gt;

&lt;p&gt;That observation is correct. The conclusion usually drawn from it — therefore, model it five times, once per context, and translate between the copies — is not the only available response, and I'd argue it's rarely the right one. When a single word is doing genuinely different jobs in different parts of the business, that is usually evidence that it was never one concept to begin with. It's evidence of exactly the same mistake &lt;code&gt;Book&lt;/code&gt; made before &lt;code&gt;Loan&lt;/code&gt; was extracted from it — a name covering more than one responsibility — except at a larger scale, and instead of doing the extraction (naming the actual underlying concepts: a contract, a claim case, a reserve calculation — each with its own identity, its own lifecycle, connected by ordinary references, the same way &lt;code&gt;Order&lt;/code&gt;, &lt;code&gt;Invoice&lt;/code&gt;, and &lt;code&gt;Shipment&lt;/code&gt; are three objects rather than three departments' versions of one), the bounded-context move keeps the original, overloaded name in every room and adds a translation layer at each door. That's not respecting the business's multiple truths. It's declining to find out what the business's multiple truths are actually called.&lt;/p&gt;

&lt;p&gt;This is worth stating plainly, because it's easy to mistake for a concession it isn't: the deeper the semantic divergence, the &lt;em&gt;more&lt;/em&gt; extraction work is implied, not less — and the more reason to do it inside one model, where the newly-named concepts can still reference each other directly, rather than across a boundary that forces every relationship between them through an anti-corruption layer. A reinsurance contract and the claim filed against it are obviously different things with different lifecycles; that's an argument for &lt;code&gt;ReinsuranceContract&lt;/code&gt; and &lt;code&gt;ClaimCase&lt;/code&gt; as two well-named, related objects, not for two disconnected "Policy" models maintained by two teams who've agreed never to look directly at each other's data. Genuine semantic depth is the strongest case &lt;em&gt;for&lt;/em&gt; doing the modeling work, not the exception that excuses skipping it.&lt;/p&gt;

&lt;p&gt;None of this is an argument that physical distribution is always wrong. There are real, legitimate reasons to run things as separate deployable units: independent failure isolation that actually matters operationally, genuinely independent scaling needs, regulatory requirements that mandate separation for audit or compliance reasons unrelated to modeling at all. The test for whether a split like that is healthy is simple, and it's the same test from the start of this article: &lt;strong&gt;does the domain model have to change shape to accommodate the split?&lt;/strong&gt; If the answer is no — if the same concepts, the same responsibilities, the same rules hold, and only the mechanism for reaching across them changes from a method call to a network call — then the split is a free, reversible decision about deployment, made after the model earned the right to be trusted, and accidental complexity is correctly staying downstream of essential complexity. If the model &lt;em&gt;does&lt;/em&gt; have to change shape — if concepts get duplicated, renamed per-context, or translated through an anti-corruption layer to paper over a divergence nobody actually investigated — then the split came first, and the modeling work that should have preceded it never happened. The boundary became a substitute for understanding, not a consequence of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring the wrong thing very precisely
&lt;/h2&gt;

&lt;p&gt;A reasonable objection at this point: surely modern engineering practice catches this. Code review, static analysis, test coverage gates, architecture review boards — surely all of this machinery exists to prevent exactly the kind of drift described above.&lt;/p&gt;

&lt;p&gt;It doesn't, and it's worth being precise about why, because the machinery is not useless — it's aimed at a different target entirely. A static analyzer can tell you a method is too long, that a class has too many dependencies, that cyclomatic complexity has crossed a threshold. None of that is a domain question. SonarQube has no opinion on whether &lt;code&gt;Customer&lt;/code&gt; should hold a &lt;code&gt;preferredCarrier&lt;/code&gt; field directly or delegate that entirely to a &lt;code&gt;CustomerShipping&lt;/code&gt; object that doesn't exist on &lt;code&gt;Customer&lt;/code&gt; at all, because that isn't a code-smell question, it's a question about whether the model corresponds to how the business actually works — and no tool that operates on syntax has any way to check a fact that only exists in a domain expert's head.&lt;/p&gt;

&lt;p&gt;So an organization can run an elaborate, expensive process — fully pipelined microservices, every commit reviewed, every merge gated on a green static analysis run, deployment fully automated — and produce, at the end of all of it, a system whose model is confidently, fluently, rigorously wrong. Every visible signal says the engineering is going well, because every visible signal is measuring implementation hygiene, and implementation hygiene and model correctness are different axes that happen to get conflated constantly, because rigor &lt;em&gt;feels&lt;/em&gt; like one thing.&lt;/p&gt;

&lt;p&gt;This connects back to where the article started. Nothing in the standard toolkit is built to catch a violation of any of the three conditions this article has been tracing — none of them are code-smell questions, and no linter has an opinion on whether essential complexity stayed whole, gave feedback, or remained cheap to correct. The absence of a controlled alternative means a team can run this kind of theater for years, ship working software the whole time, and never learn that a few small, ordinary classes — built around the right concepts instead of the existing process — would have outperformed all of it. A well-designed model with mediocre implementation has a much higher ceiling than a brilliantly implemented wrong one, because the brilliance in the second case is mostly being spent compensating for the model — defensive checks for cases that shouldn't exist, synchronization between copies of state that never needed to be duplicated, translation layers between contexts that never needed separating — and all of that compensating effort gets thrown away the moment someone finally corrects the model underneath it. Effort spent on a correct model compounds. Effort spent on an incorrect one partially evaporates, no matter how rigorously it was reviewed on the way in.&lt;/p&gt;

&lt;p&gt;Good engineering practice, by this account, is not the pipeline. It's the discipline of being able to say, clearly, what the model is and why — the implementation afterward is the easy part, and it has always been the easy part. The pipeline measures the easy part very thoroughly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this gets more urgent, not less, with AI
&lt;/h2&gt;

&lt;p&gt;Here is the part that didn't apply five years ago in quite the same way.&lt;/p&gt;

&lt;p&gt;Implementation has historically had a floor of friction underneath it that nudged people toward structure almost by accident. Hacking procedural code together against a complex domain became unmanageable quickly enough — the special cases piled up, the conditionals nested, the same logic got copy-pasted into three places — that developers were pushed toward extracting structure out of self-preservation, even teams who'd never read a line of object-oriented theory. The friction wasn't a deliberate teacher, but it taught something, by making the wrong path visibly painful to keep walking.&lt;/p&gt;

&lt;p&gt;AI-assisted coding removes a great deal of that friction — and it's worth being precise about what that means, because "AI breaks the feedback loop" is a slightly different and less accurate claim than what's actually happening. AI doesn't break the loop. It removes the pressure that used to force the loop into existence in the first place, often for teams who never deliberately chose it and couldn't have named it if asked. Take that pressure away and the loop doesn't vanish — it just stops being automatic. From here on, keeping it is a deliberate choice, the same as any discipline that doesn't enforce itself.&lt;/p&gt;

&lt;p&gt;But there's a second, subtler effect that goes beyond friction removal, and it maps directly onto all three conditions this article has been tracing. Consider what happens when a library system needs to lend DVDs. A human developer who adds &lt;code&gt;DVD&lt;/code&gt; as a sibling of &lt;code&gt;Book&lt;/code&gt;, then adds &lt;code&gt;Vinyl&lt;/code&gt; six months later, then writes an increasingly complex query to aggregate loan counts across three separate entity types — that developer &lt;em&gt;feels&lt;/em&gt; something. Not necessarily consciously, and probably not articulately. But the query is harder to write than it should be. The next story that touches lending takes longer than expected. Something resists. That resistance is a weak signal, easily ignored and often misattributed to "the domain is just complex," but it exists. Occasionally it prompts a conversation, a refactor, or a senior developer asking why this feels harder than it should. It is, in a loose and informal way, the model giving feedback through the second condition.&lt;/p&gt;

&lt;p&gt;AI generates the three-way join with exactly the same fluency as the one-way query. It doesn't experience resistance. The code is clean, the tests pass, the feature ships. Nobody in the process felt anything. The signal that a wrong shape generates — growing complexity, queries that accumulate joins, stories that quietly take longer than they should — exists nowhere in the experience of either the AI or the prompter, who is working at a level of abstraction that sees "does this feature work," not "is this implementation getting harder than it ought to be." The feedback that used to live in development, however weakly, has moved entirely to production: corrupt data, incoherent transactions, a simple-sounding feature that turns out to require three months because nobody can find a clean place to put it in a model nobody shaped for it. That's the most expensive place for a feedback loop to live, and it's where AI pushes everything — not just for procedural code or bounded contexts, but for any approach that wasn't built around a model designed to give feedback structurally rather than through the developer's pain.&lt;/p&gt;

&lt;p&gt;That question gets answered in a refinement session, by watching a domain expert's reaction to a model that doesn't quite match what's in their head, by treating a new user story as evidence rather than as an instruction. AI has no access to that room. It can implement what it's told with great fluency, but it has no mechanism for discovering that what it was told was an incomplete or slightly wrong description of the domain, because discovering that requires exactly the adversarial, repeated checking against reality that this entire article has been describing as the actual function of a domain model. A model built without that checking is not a faster way to get to a correct system. It's a faster way to arrive, confidently and with clean code, at the same unfalsifiable mistake the rest of the industry has been making for decades — just produced at a speed that makes it considerably harder to notice before the cost compounds.&lt;/p&gt;

&lt;p&gt;The bottleneck in software quality was never really implementation, even before AI; it only looked that way because implementation was the part that consumed the most visible hours. Collapse the cost of those hours toward zero, and what's left, undisguised, is the question that was always the only one that mattered: did anyone actually understand what they were building, or did they just build the first plausible shape it was described as, and call it done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Essential complexity, made tangible and testable
&lt;/h2&gt;

&lt;p&gt;A rich domain model is a tool. A tool to learn what a domain actually is, a tool to define it precisely enough that implementation stops being a guess, a tool to document it in a form that has to keep working, because unlike a wiki page, it can't silently drift out of date without a compiler, or a database constraint, saying so. It is essential complexity made tangible — something you can point at — and testable — something that tells you, specifically and immediately, the moment it's wrong.&lt;/p&gt;

&lt;p&gt;Everything in this article has really been one long demonstration of what happens when the three conditions that tool depends on get broken, one at a time. Split a domain along functional lines that made sense given what was known at the time, and every scenario you already knew about still works — but the cross-cutting rule that arrives later, whether it's a compliance deadline or an ordinary business decision nobody had thought of yet, now costs an integration project instead of a few small classes, because the domain that should have stayed whole was cut before anyone could know what would eventually need to reach across the cut. Disperse the logic into fat services and repositories and DTOs instead, and the model stops giving feedback at all, because there's no longer one place for a wrong assumption to collide with itself and be caught. Hand the implementation to something that writes fluent code without ever asking whether the shape it was given was the right one, and the loop that used to force discovery — slowly, expensively, but eventually — stops being forced. It doesn't disappear. It just stops happening unless someone chooses, deliberately, to make it happen.&lt;/p&gt;

&lt;p&gt;Which is where this circles back to where it started. Software architecture is unfalsifiable — no control group, no alternative built alongside the one that shipped, every conclusion drawn from an experience of one. That problem isn't going away. But a rich domain model is the closest substitute available for the experiment nobody gets to run: not proof that a decision was right, but a running, continuous test of whether it still is — for as long as the essential complexity stays whole enough to look at, gives feedback when it's wrong, and stays cheap enough to correct that correcting it remains something a team will actually do, rather than something they agree, in principle, they should.&lt;/p&gt;

&lt;p&gt;None of this requires architects and engineers to want it to be true. That's the uncomfortable part, and it's worth ending on. The costs of drifting away from it — the fat service, the boundary drawn early, the AI-fluent implementation of a shape nobody examined — are deferred, distributed across people who didn't make the original decision, and individually invisible at the moment each one gets made. Nobody sets out to make software hard to change. They choose a service split that solves this quarter's problem, a pattern from a conference talk, a completion that passes the tests in front of them. The same unfalsifiability that opened this article is exactly why none of those choices announce themselves as mistakes at the time — there's no control group showing what the alternative would have looked like. A rich domain model doesn't argue anyone out of making those choices. It just makes the cost of having made them visible while the bill is still small enough to pay.&lt;/p&gt;

&lt;p&gt;The purpose of a rich domain model is not to be right. It is to make being wrong visible while the cost of correction remains small.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>java</category>
      <category>softwaredevelopment</category>
      <category>architecture</category>
    </item>
    <item>
      <title>AntiPatterns Never Left, We Just Stopped Calling Them by Name</title>
      <dc:creator>Leon Pennings</dc:creator>
      <pubDate>Mon, 15 Jun 2026 12:10:54 +0000</pubDate>
      <link>https://dev.to/leonpennings/antipatterns-never-left-we-just-stopped-calling-them-by-name-969</link>
      <guid>https://dev.to/leonpennings/antipatterns-never-left-we-just-stopped-calling-them-by-name-969</guid>
      <description>&lt;p&gt;In 1998, a book called &lt;em&gt;AntiPatterns&lt;/em&gt; did something unusual: instead of cataloguing good solutions to recurring problems, it catalogued &lt;em&gt;bad&lt;/em&gt; ones — the recognizable, recurring ways software projects go wrong. The Blob, Spaghetti Code, Stovepipe Enterprise, Mushroom Management. Each one came with a name, a description of the symptom, and a refactored path out.&lt;/p&gt;

&lt;p&gt;Patterns and AntiPatterns are two sides of the same coin. A pattern says: here's a known problem, and here's a solution that tends to work. An AntiPattern is not simply "a bad solution" — it's a description of a recurring &lt;em&gt;failure mode&lt;/em&gt;, or of something that actively blocks or resists effective development, even when (especially when) it doesn't look like a mistake at the time.&lt;/p&gt;

&lt;p&gt;What makes an AntiPattern dangerous isn't that it's obviously wrong. It's that the failure mode it describes tends to be &lt;strong&gt;invisible while it's happening&lt;/strong&gt;. This is the &lt;strong&gt;unfalsifiability problem&lt;/strong&gt;: if a system works, meaning it runs, it ships and does what it should do, the choice that produced it gets read as validated. The counterfactual (what if we'd done it differently?) is invisible. Nobody runs that experiment. So the failure mode doesn't get diagnosed; it gets repeated, often by other teams, often with conviction, often dressed up as best practice.&lt;/p&gt;

&lt;p&gt;Patterns have an entire consulting industry built around teaching them. AntiPatterns, as far as we can tell, mostly don't — there's no equivalent industry whose job is to walk into a project and say "this is Stovepipe Enterprise, and here's what it'll cost you in three years." So the old catalogue — genuinely old now, pre-dating microservices, Kubernetes, Spring Boot, Scrum-as-religion, and the entire modern cloud-native stack — quietly fell out of view. Not because the failure modes it described went away, but because nobody was selling the diagnosis.&lt;/p&gt;

&lt;p&gt;Going back to that old catalogue, the question is simple: which of these still apply, and to what, today? The answer, overwhelmingly, was: almost all of them, just wearing different clothes. What follows is sourced from the originals, regrouped into four themes, each of which is really just a different &lt;em&gt;altitude&lt;/em&gt; at which the unfalsifiability problem operates — from the codebase, to the organization, to the industry at large.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 1: Technical Axis vs. Domain Axis
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;The shape of unfalsifiability here: a working system hides which axis its structure is organized around — until the domain changes, and you discover the boundaries were drawn for the compiler's convenience, not the business's.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Software has at least two legitimate ways to be sliced. One is by &lt;em&gt;what the business cares about&lt;/em&gt; — a Risk Summary, a Customer Account, an Order. The other is by &lt;em&gt;technical concern&lt;/em&gt; — repositories, services, controllers, DTOs, event handlers. Both are real. The trouble starts when the technical axis becomes the organizing principle and the domain concepts get fragmented across it, because nobody's job is to keep the &lt;em&gt;domain&lt;/em&gt; concept coherent anymore — everybody's job is to keep their &lt;em&gt;layer&lt;/em&gt; coherent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jumble → "Accidental Layering"
&lt;/h3&gt;

&lt;p&gt;The original Jumble AntiPattern describes what happens when horizontal layers (presentation, business logic, data access) and vertical domain slices get intermixed without discipline, producing an architecture that's neither cleanly layered nor cleanly domain-partitioned.&lt;/p&gt;

&lt;p&gt;The modern, much more common version of this is subtler and looks like &lt;em&gt;good practice&lt;/em&gt;: "put all your queries in a repository layer." On the surface this is just separation of concerns. In practice, it means a concept like "risk total for this category" — which is meaningful only in the context of a Risk Summary — gets implemented as a generic, context-free query method sitting in a repository, available to be called from anywhere, by anything, with no memory of what it's &lt;em&gt;for&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The usual defense is reuse: "if the query lives in the repository, every part of the system that needs a risk total can call the same method." This sounds reasonable until you notice what's actually being reused. &lt;strong&gt;You never reuse a query — you reuse what it represents.&lt;/strong&gt; A query is just an implementation detail; "risk total for this category" is the &lt;em&gt;concept&lt;/em&gt; that needs to stay consistent. Reusing the query method gives you textual reuse of some SQL. Reusing the Risk Summary object — calling its &lt;code&gt;riskTotal()&lt;/code&gt; — gives you reuse of the &lt;em&gt;context&lt;/em&gt;: the rules about what counts, what's excluded, how categories nest, all of it living in one place that knows what "risk total" means.&lt;/p&gt;

&lt;p&gt;The failure mode this produces is depressingly specific and common: need A comes along, and the existing query in the repository is &lt;em&gt;almost&lt;/em&gt; right but not quite — so rather than fix the shared query (and risk breaking need B, which also calls it), whoever's implementing A copies the query and tweaks it to fit. Now there are two queries called "risk total," subtly different, and nothing in the codebase says they're supposed to mean the same thing — or that they don't anymore.&lt;/p&gt;

&lt;p&gt;The opposite also happens, and it's arguably worse. Need A is &lt;em&gt;slightly&lt;/em&gt; different from need B, but whoever's working on A assumes they're the same — same name, same shape, looks like the same query — and edits the shared one in place to fit A's requirements. The unit test for B never anticipated this scenario, because nobody writing it imagined "someone will later assume this is also A's query and change it accordingly." So B doesn't break across the board; it breaks in &lt;em&gt;some&lt;/em&gt; scenarios — the ones where A's and B's actual requirements diverge — which is exactly the kind of bug that surfaces in production, intermittently, long after the change, and gets debugged as "weird edge case" rather than traced back to a shared query that two different concepts were silently sharing.&lt;/p&gt;

&lt;p&gt;Both directions — forking a query that should've stayed shared, and editing a shared query that should've stayed forked — have the same root cause: there's no explicit object whose job it is to &lt;em&gt;own&lt;/em&gt; the concept and represent the boundary between what A needs and what B needs. A unit test won't catch either, and this is where it loops back to unfalsifiability directly: a test only encodes what was known &lt;em&gt;at the time it was written&lt;/em&gt;. The missing context — that A and B are both expressions of the same domain concept, and a change to one is a change to the meaning of the other, &lt;em&gt;or&lt;/em&gt; that they aren't and a change to one must not touch the other — is exactly the thing a context-free query can't carry and a test can't recover after the fact. Keep the query on the Risk Summary, as a method on the aggregate that owns the concept, and both classes of bug become structurally harder to write — not because anyone's more careful, but because there's only one place "risk total" can live, and changing it visibly changes everything that depends on it.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;accidental layering&lt;/strong&gt;: structure that exists to organize the technology — how do we talk to the database — at the cost of fragmenting the domain concepts that the technology is supposed to be serving. The repository version &lt;em&gt;works&lt;/em&gt;. It compiles, it returns data, the tests pass. The cost only shows up later, when "risk total" quietly stops meaning one thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Functional Decomposition + Poltergeists → The Anemic Domain Model
&lt;/h3&gt;

&lt;p&gt;The original Functional Decomposition AntiPattern describes experienced procedural developers writing object-oriented code that's secretly still procedural — classes exist, but they're really just namespaces for functions, operating on data that lives elsewhere. Poltergeists, in the same vein, are short-lived classes whose only job is to kick off a process for some other object and then disappear.&lt;/p&gt;

&lt;p&gt;Put these two together and you get a near-perfect description of the "fat service, thin object" shape that shows up across most layered architectures, regardless of language or framework: &lt;code&gt;Service&lt;/code&gt; classes full of methods that orchestrate behavior, operating on &lt;code&gt;Entity&lt;/code&gt; objects and DTOs that are really just data bags with getters and setters. The class structure is object-oriented. The &lt;em&gt;behavior&lt;/em&gt; is procedural — it's Pascal with annotations, or Pascal with decorators, or Pascal with whatever the local ceremony happens to be. The "objects" don't do anything; the services do everything &lt;em&gt;to&lt;/em&gt; the objects.&lt;/p&gt;

&lt;p&gt;Layered on top of this, the Poltergeists are everywhere: mapper classes that convert entities to DTOs and back, one-shot orchestrator classes, &lt;code&gt;*Factory&lt;/code&gt; and &lt;code&gt;*Builder&lt;/code&gt; and &lt;code&gt;*Handler&lt;/code&gt; classes whose entire lifecycle is "get instantiated, shuttle control from the controller to the service to the repository, disappear." None of these classes &lt;em&gt;know&lt;/em&gt; anything. They just move data and call the next thing.&lt;/p&gt;

&lt;p&gt;This is the architecture that "put the logic in the service, keep the data in the DTO" produces by default — not because any particular framework forces it, but because it's the path of least resistance once logic and data have been separated by convention, and the path of least resistance is what most codebases end up looking like at scale. The object-oriented vocabulary (classes, methods, "services") is all there. What's missing is anything that resembles an &lt;em&gt;object&lt;/em&gt; in the original sense — something that owns both its data and the rules about what that data means, the way the Risk Summary above owns &lt;code&gt;riskTotal()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lava Flow → Dead Artifacts in Event-Driven Architecture
&lt;/h3&gt;

&lt;p&gt;The original Lava Flow AntiPattern is about dead code and forgotten design decisions that get frozen into an ever-changing codebase — like hardened rock in a lava field, nobody quite remembers how it got there, and nobody's confident enough to remove it.&lt;/p&gt;

&lt;p&gt;Event-driven architecture and CQRS can become extremely effective Lava Flow generators. The mechanism is specific: when a state change happens, it gets translated into an event, published, and then &lt;em&gt;handled&lt;/em&gt; — possibly by several consumers, possibly asynchronously, possibly with retries, possibly written to an outbox first. Each of those steps is a place where a "rock" can harden: a handler that nobody triggers anymore because the upstream condition that used to fire it was refactored away, a published event type that three downstream services still subscribe to "just in case," a saga step that's technically unreachable but nobody's sure enough to delete it.&lt;/p&gt;

&lt;p&gt;The original Lava Flow's solution was a configuration management process that actively hunts down and eliminates dead code. The EDA version of dead code is much harder to hunt, because it isn't sitting in one file you can search for — it's a &lt;em&gt;subscription&lt;/em&gt;, a &lt;em&gt;topic&lt;/em&gt;, a &lt;em&gt;handler registration&lt;/em&gt;, possibly in a different repository than the thing that used to trigger it. The debris isn't dead code in the traditional sense; it's dead &lt;em&gt;connections&lt;/em&gt; — and because messaging is fundamentally fire-and-forget, those dead connections don't even fail loudly. A handler nobody needs anymore doesn't throw; it just keeps running, on schedule, consuming compute and network for events that no longer mean anything to anyone. In a monolith, dead code is at least &lt;em&gt;inert&lt;/em&gt; — it sits there, unused, until someone deletes it. In EDA, dead code can be &lt;em&gt;active&lt;/em&gt;: a ghost process, still executing, still costing money, with no stack trace and no error to tell you it's a ghost.&lt;/p&gt;

&lt;p&gt;But there's a cost that shows up even while everything is alive and healthy, which is arguably the more important one: the &lt;em&gt;dependency itself&lt;/em&gt; becomes invisible. When A's state change causes B's state change in the same transaction, that causality is right there in the code — a call, a method, something you can read and step through. When A publishes an event and B (eventually, somewhere) handles it, that same causality still exists — B still depends on A having happened — but it no longer exists &lt;em&gt;anywhere in the code&lt;/em&gt;. It exists only as a runtime fact: a subscription, a topic name, a piece of configuration. Debugging "why did B happen" stops being a matter of reading code and becomes a matter of reconstructing a causal chain after the fact, across services, via logs, correlation IDs, and timestamps.&lt;/p&gt;

&lt;p&gt;That reconstruction can be done — distributed tracing, log aggregation, and correlation IDs all exist precisely to make it possible — but it's worth being honest about what those tools &lt;em&gt;are&lt;/em&gt;: compensating machinery, built to recover something a single transaction would have given you for free. A lot of what gets called "modern observability" is, functionally, the cost of paying back the contextualization that decoupling spent. Keeping things that should happen together actually &lt;em&gt;together&lt;/em&gt; — same domain object, same transaction, even across multiple methods — doesn't require any of that machinery, and is a lot less likely to generate a Lava Flow in the first place, because there's nothing to subscribe to, lose track of, or reconstruct.&lt;/p&gt;

&lt;p&gt;It's also worth separating two things that get conflated under "we need EDA/microservices to scale": scaling and splitting are not the same operation. A monolith can scale horizontally — more instances behind a load balancer — without anything being split apart at all. Splitting a system into services that communicate via events solves a &lt;em&gt;coordination&lt;/em&gt; problem (independent deployability, team ownership, different parts needing different resource shapes) — it doesn't, by itself, make anything handle more load. When "we need to scale" is used to justify "therefore we need to split," a capacity problem is being answered with an architecture decision that's actually about organizational boundaries — which may be the right call, but is a different call, justified by different reasons, with the debugging and Lava Flow costs described above as part of its price.&lt;/p&gt;

&lt;p&gt;When a state change happens close to the domain core — same object, same transaction, even if not the same method — it's visible. When it happens by firing an event across a transactional boundary, you've traded visibility for decoupling, and the Lava Flow is the price of that trade, paid later, by someone else, in a form that doesn't even announce itself as a cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vendor Lock-In → Framework Lock-In Without a Vendor
&lt;/h3&gt;

&lt;p&gt;The original Vendor Lock-In AntiPattern describes systems that become highly dependent on a proprietary architecture, to the point where switching away becomes prohibitively expensive — historically, think IBM mainframes, or any single-vendor enterprise stack.&lt;/p&gt;

&lt;p&gt;The interesting modern twist is that lock-in no longer requires a &lt;em&gt;vendor&lt;/em&gt; in the old sense — and then, almost as if to prove the original AntiPattern's point all over again, the vendor relationship quietly grows back. Spring is open source, but the company behind it now sells exactly the kind of commercial support arrangement Vendor Lock-In originally warned about: OSS minor releases get a guaranteed support window of just over a year, after which the application keeps running on the last published artifact, but any newly discovered vulnerabilities have no upstream fix — you're on your own unless you pay for extended coverage.&lt;/p&gt;

&lt;p&gt;So the choice, every year or so, per major dependency line, is: pay for enterprise support, or pay in engineering time to upgrade. And "pay in engineering time" is real money with a real number attached — a couple of contractors spending a chunk of their month on framework version bumps, dependency conflict resolution, and re-testing everything that touches the upgraded pieces, adds up to a bill that's directly comparable to a support contract, except it's hidden inside "maintenance" rather than itemized as "vendor cost." Either way, you're paying &lt;em&gt;someone&lt;/em&gt; to keep the substrate underneath you current — which is the textbook definition of dependency, just relabeled.&lt;/p&gt;

&lt;p&gt;On top of that: a sufficiently "Spring-native" codebase — laced with &lt;code&gt;@Autowired&lt;/code&gt;, &lt;code&gt;@Transactional&lt;/code&gt;, &lt;code&gt;@Service&lt;/code&gt;, component scanning, and the conventions that make all of that work — is &lt;em&gt;enormously&lt;/em&gt; expensive to extract from regardless of who you're paying. Not because anyone's charging you to leave, but because your domain logic and the framework's lifecycle have become structurally entangled. The framework isn't a dependency you call; it's the substrate your code lives inside.&lt;/p&gt;

&lt;p&gt;This matters because none of it &lt;em&gt;feels&lt;/em&gt; like the lock-in the original AntiPattern described. There's a contract now — but it's framed as "support," not as the price of staying put. It feels like "just using a popular, well-supported framework, with optional extras" — which is exactly what makes it durable. The unfalsifiability problem here is almost total: there is no single event that tells you "you are now locked in." You just slowly become unable to imagine the alternative, the renewal invoice (or the upgrade sprint) arrives on schedule, and the system keeps working — so the question of whether this is actually cheaper than the alternative never gets asked, let alone answered.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 2: Adoption Without Evaluation
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;The shape of unfalsifiability here: at the industry scale, popularity itself becomes the evidence. The road not taken is invisible, so "widely adopted" quietly substitutes for "evaluated and found correct for this context."&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Continuous Obsolescence → Dialect Drift Inside "the Same Language"
&lt;/h3&gt;

&lt;p&gt;The original Continuous Obsolescence AntiPattern is about ecosystem churn: technology moves fast enough that finding compatible versions of things that actually interoperate becomes its own ongoing project, and developers spend real effort just keeping the floor from shifting under them.&lt;/p&gt;

&lt;p&gt;There's a related but distinct failure that the original framing doesn't quite capture, and Scala is the clearest historical example of it. Scala's problem was never really "too many releases" — it was that the language gave every team enough expressive power (implicits, operator overloading, macros, DSL-building features) to define its own dialect. Walking into a new Scala codebase often meant &lt;em&gt;learning that codebase's Scala&lt;/em&gt; before you could be productive in it, on top of learning Scala itself. The language was technically one language; in practice it was as many languages as there were teams willing to use its more expressive corners.&lt;/p&gt;

&lt;p&gt;Java spent a long time being the opposite of this on purpose — verbose, explicit, "English-like," deliberately leaving little to the imagination, precisely so that a Java codebase from one team looked recognizably like a Java codebase from another. But the last decade of Java releases — lambdas, streams, &lt;code&gt;var&lt;/code&gt;, records, sealed types, pattern matching, and the steady cultural push toward "boilerplate reduction" — has been adding exactly the kind of expressive, compact, &lt;em&gt;idiomatic&lt;/em&gt; features that Scala had from day one. None of these features are bad in isolation. But each one raises the floor of what "reading Java" requires, and — just like Scala — different codebases adopt different subsets of them, idiomatically or not, without anyone deciding this as policy. A codebase built around streams-of-records-with-pattern-matching reads nothing like one that's still mostly loops and getters, even though both compile as "just Java 21." The dialect fragmentation Scala had in the open, Java is quietly acquiring feature-by-feature, each addition individually justified as "less boilerplate," with nobody tracking the cumulative effect on how many distinct &lt;em&gt;styles&lt;/em&gt; of Java a developer now needs to be fluent in before "knowing Java" actually means being productive.&lt;/p&gt;

&lt;p&gt;This is Continuous Obsolescence at the level of &lt;em&gt;readability&lt;/em&gt; rather than &lt;em&gt;dependency versions&lt;/em&gt; — the floor for entry-level legibility keeps rising, a release at a time, and because each individual feature is small and well-intentioned, there's never a single moment where anyone evaluates whether the codebase as a whole still meets its own bar for "anyone on the team can read this."&lt;/p&gt;

&lt;h3&gt;
  
  
  Golden Hammer
&lt;/h3&gt;

&lt;p&gt;The original Golden Hammer is the most literal of the bunch and barely needs updating: a familiar technology or concept, applied obsessively to problems it doesn't fit, because it's the tool the team knows. The original's prescribed fix — expand developers' knowledge through education, training, and book study groups, so they have &lt;em&gt;alternatives&lt;/em&gt; to reach for — is, charmingly, still the prescribed fix in 2026, and still mostly doesn't happen.&lt;/p&gt;

&lt;p&gt;What's changed is the scale of the hammer. In 1998 a Golden Hammer might be one design pattern, applied everywhere. Today it's an entire &lt;em&gt;platform&lt;/em&gt; — Kubernetes for a five-person team's internal tool, Kafka because the last company used Kafka, a service mesh for an application with three services. The hammer got bigger, but the mechanism — familiarity substituting for fit — is identical.&lt;/p&gt;

&lt;p&gt;Continuous Obsolescence and Golden Hammer are, in a sense, mirror images. Golden Hammer is under-using a toolkit's diversity — one familiar tool, applied everywhere, regardless of fit. Dialect drift is over-diversifying a &lt;em&gt;language's&lt;/em&gt; feature usage until the codebase itself becomes a toolkit nobody fully knows — every corner adopted because it was available and looked like an improvement, with nobody asking whether the codebase, as a whole, was better off with a smaller, more uniform set of idioms. Same lack of deliberateness, opposite direction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture by Implication → Survivorship Bias as Architecture
&lt;/h3&gt;

&lt;p&gt;The original AntiPattern describes overconfidence carried forward from past successes: a general approach that worked once gets applied to the next system, without anyone checking whether the new system's risks and requirements are actually similar.&lt;/p&gt;

&lt;p&gt;The deeper version of this: every system is, in practice, only ever built &lt;em&gt;once&lt;/em&gt;. The cheaper, simpler alternative was never actually built, so there's no comparison to make. If the system that &lt;em&gt;was&lt;/em&gt; built works, that gets read as success — full stop. Nobody can point to the parallel universe where the team built the boring monolith instead of the microservices, or skipped CQRS, or didn't introduce the event bus, and ask whether &lt;em&gt;that&lt;/em&gt; version would have shipped faster, cost less, and been easier to change.&lt;/p&gt;

&lt;p&gt;This is why patterns like EDA, CQRS, and microservices — which have entirely legitimate origin contexts (genuinely high scale, genuinely independent teams, genuinely eventual-consistency-tolerant domains) — end up applied far outside those contexts. The pattern &lt;em&gt;worked&lt;/em&gt; somewhere, visibly, loudly, in a conference talk. The boring alternative never got a conference talk, because it was boring, because nothing went wrong, because there was nothing to present. "It shipped and the company didn't die" gets read as validation of the &lt;em&gt;pattern&lt;/em&gt;, when it's really just validation that the constraints were tolerable — which tells you nothing about whether the pattern was &lt;em&gt;necessary&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Intellectual Violence → Complexity as Social Leverage
&lt;/h3&gt;

&lt;p&gt;The original AntiPattern describes someone who understands a theory, technology, or buzzword using that knowledge to intimidate others in a meeting — winning the argument not on merits, but by making disagreement look like ignorance.&lt;/p&gt;

&lt;p&gt;The modern version doesn't even require an intimidator. CQRS, Dependency Injection, Event-Driven Architecture — these are genuinely complex enough that &lt;em&gt;disagreeing&lt;/em&gt; with their use requires demonstrating you understand them well enough to critique them specifically. "I don't think we need this" sounds, to a room that's already nodding, indistinguishable from "I don't understand this." So the safer move — for almost everyone in the room — is to nod too. The complexity itself does the intimidating; nobody has to play the difficult one in the room.&lt;/p&gt;

&lt;p&gt;This connects directly to the rest of the chapter. Continuous Obsolescence and Golden Hammer explain &lt;em&gt;what&lt;/em&gt; gets adopted — features and tools chosen for familiarity or availability rather than fit. Architecture by Implication explains why nobody's &lt;em&gt;checking&lt;/em&gt; whether the adoption was the right call (no visible counterfactual). Intellectual Violence explains why, even when someone privately &lt;em&gt;suspects&lt;/em&gt; it wasn't the right call, they don't say so out loud. Different mechanisms, same outcome: complexity that nobody individually chose, but everyone collectively rubber-stamped.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 3: The Cure Regrows the Disease
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;The shape of unfalsifiability here: at the organizational scale, a structural fix is judged purely by whether the system still works afterward — not by whether the underlying disease actually left, or just moved to an organ nobody's looking at.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Spaghetti Code → Distributed Spaghetti
&lt;/h3&gt;

&lt;p&gt;The original Spaghetti Code AntiPattern is the classic: ad hoc structure, no clear flow, difficult to extend or optimize, fixable mainly through disciplined, ongoing refactoring.&lt;/p&gt;

&lt;p&gt;Microservices are frequently sold as the cure for this — break the tangled monolith into small, independent, individually-comprehensible services. And at the scale of a single service, that's often true: a small service &lt;em&gt;can&lt;/em&gt; be spaghetti-free in a way a 100-object monolith struggles to be.&lt;/p&gt;

&lt;p&gt;But the failure mode doesn't require any single service to be tangled. It requires the &lt;em&gt;system&lt;/em&gt; to be tangled — and distributing the tangle across network boundaries doesn't untangle it, it just makes each individual strand harder to see and far more expensive to follow. Spaghetti Code was always survivable, in part, &lt;em&gt;because the compiler caught some of it&lt;/em&gt; — a method signature change that breaks twelve callers is a build failure, immediately, locally, before anything ships. Distributed spaghetti loses that safety net entirely: the equivalent change is a contract change between services, the breakage is a runtime error in production, possibly in a service owned by a different team, possibly days later.&lt;/p&gt;

&lt;p&gt;The decomposition didn't remove the failure mode. It changed its blast radius — and traded a problem you could &lt;em&gt;see&lt;/em&gt; (a big tangled codebase, sitting right there, clearly someone's problem) for one you mostly can't (a tangle of contracts and assumptions spread across services and teams, nobody's full-time job to track).&lt;/p&gt;

&lt;h3&gt;
  
  
  Stovepipe Enterprise → Microservices and the Ossified Boundary
&lt;/h3&gt;

&lt;p&gt;The original Stovepipe Enterprise describes a &lt;em&gt;lack&lt;/em&gt; of coordination and planning across systems — each one solving its own slice in isolation, duplicating effort, creating integration headaches with everyone else.&lt;/p&gt;

&lt;p&gt;Microservices done at the boundary level produce something that looks like the opposite problem but shares the same root cause. The boundaries get drawn carefully, thoughtfully, with real coordination — at one point in time, based on the team's &lt;em&gt;current&lt;/em&gt; understanding of how the business works. And then the business — its processes, its terminology, its rules about what belongs together — keeps changing, because that's what businesses do. The &lt;em&gt;code&lt;/em&gt; structure ossifies around a snapshot of domain understanding that the domain itself has already moved past.&lt;/p&gt;

&lt;p&gt;The original Stovepipe Enterprise is mostly about &lt;em&gt;waste and duplication from never coordinating&lt;/em&gt;. The microservices version is almost the inverse symptom from the same underlying mistake: treating the system as a sum of independently-evolvable parts, when the thing that actually needs to evolve — the shared understanding of the domain — doesn't respect the part boundaries at all. It's easy to split a system along today's understanding. It's extremely hard to &lt;em&gt;un&lt;/em&gt;-split it when that understanding changes, which is exactly when you'd need to.&lt;/p&gt;

&lt;p&gt;None of this means microservices are categorically wrong. It means microservices trade a maintenance cost you can &lt;em&gt;see&lt;/em&gt; — a large, tangled codebase, sitting there, undeniably someone's problem — for one that's much harder to see: accidental complexity that didn't go away, it moved to the seams between services and got a network hop attached to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Throw It Over the Wall → Platform Teams
&lt;/h3&gt;

&lt;p&gt;The original AntiPattern describes object-oriented guidelines and implementation plans — meant as flexible suggestions — getting treated as rigid mandates by the time they reach downstream developers, accumulating false authority as they pass through approval processes.&lt;/p&gt;

&lt;p&gt;The modern instance is almost a perfect mirror, but at the team level rather than the document level. DevOps, as a movement, was explicitly framed as the antidote to exactly this kind of wall-throwing: "you build it, you run it" — collapse the separation between the people who write software and the people who operate it, so nobody can throw anything over a wall because there's no wall.&lt;/p&gt;

&lt;p&gt;What actually happened, often, is that the tooling required to "build it and run it" — CI/CD pipelines, Kubernetes, observability stacks — became sophisticated enough to need its own dedicated team. That team chooses the platform (often the popular thing, see Chapter 2), builds the pipelines, and now sits between product teams and production — a new wall, one level removed, staffed by people who weren't there when the original wall was being torn down and likely don't think of themselves as a wall at all.&lt;/p&gt;

&lt;p&gt;This is the clearest example of a pattern that recurs across this entire article: &lt;strong&gt;the antidote regrows the disease in a different organ.&lt;/strong&gt; Mushroom Management (Chapter 4) gets "solved" by Scrum's Product Owner role, which recreates the intermediary. Throw It Over the Wall gets "solved" by DevOps, which recreates the department. In both cases, the system afterward &lt;em&gt;works&lt;/em&gt; — which is exactly why nobody notices the disease came back. It just moved.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 4: Who Feels the Pain Doesn't Decide
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;The shape of unfalsifiability here: at the individual/role scale, the person making a structural decision is organizationally insulated from its consequences — so they never receive the feedback signal that would tell them the decision was wrong.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Mushroom Management → Scrum's Intermediary, Reborn
&lt;/h3&gt;

&lt;p&gt;The original Mushroom Management AntiPattern describes a deliberate policy of keeping developers isolated from end users — requirements arrive second-hand, filtered through architects, managers, or analysts, who stand between the people building the thing and the people who'll use it.&lt;/p&gt;

&lt;p&gt;Modern Scrum was, in part, supposed to fix this — user stories as &lt;em&gt;conversations&lt;/em&gt;, the whole point being a direct, ongoing dialogue between the people who need something and the people building it, with the story as a prompt for discussion rather than a finished spec.&lt;/p&gt;

&lt;p&gt;In practice, the story very often becomes the instruction rather than the conversation-starter, and the Product Owner becomes the very intermediary the process was meant to remove — now institutionalized as a defined role, with its own ceremonies, sitting precisely where the "mushroom" used to sit. It's almost recursive: an AntiPattern from 1998 describing a problem, and a 2001-era process explicitly designed to address it, regrowing the same shape inside the cure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design by Committee + Grand Old Duke of York → Chickens, Pigs, and Flattened Roles
&lt;/h3&gt;

&lt;p&gt;The original Design by Committee AntiPattern is the classic standards-body failure: overly complex architecture, lacking coherence, because too many people with too little shared context (and too little personal stake) are making decisions by committee. Grand Old Duke of York, separately, observes that programming skill doesn't equate to skill in defining &lt;em&gt;abstractions&lt;/em&gt; — there are, in practice, two different skill sets (call them abstractionists and implementationists), and ignoring that distinction hurts projects.&lt;/p&gt;

&lt;p&gt;The "chickens and pigs" framing from agile folklore captures both at once: chickens have opinions about the farm but don't lay the eggs; pigs provide the bacon and feel the consequences. Design by Committee is chickens designing for pigs. Grand Old Duke of York, reframed through modern Scrum, is something slightly different and arguably worse: Scrum often &lt;em&gt;flattens&lt;/em&gt; the abstractionist/implementationist distinction entirely — in principle, anyone on the team can take the architectural decision for a given sprint, regardless of whether they have the abstraction-defining skill the original AntiPattern says is rare and distinct.&lt;/p&gt;

&lt;p&gt;The 1998 framing at least &lt;em&gt;acknowledged&lt;/em&gt; the skill gap and tried to address it through process — get the right people defining abstractions. The flattened-role version sometimes pretends the gap doesn't exist at all. Both versions, old and new, share the same underlying mechanism with Mushroom Management and Throw It Over the Wall: the people who'll live with the architectural decision, day to day, are not reliably the people making it — and the system &lt;em&gt;working&lt;/em&gt; afterward doesn't tell you whether it was the &lt;em&gt;right&lt;/em&gt; decision, only that it wasn't an immediately &lt;em&gt;fatal&lt;/em&gt; one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Corncob
&lt;/h3&gt;

&lt;p&gt;The original AntiPattern is blunt: a Corncob is a difficult person who obstructs and diverts the development process, typically dealt with through tactical, operational, or strategic organizational maneuvering rather than direct confrontation.&lt;/p&gt;

&lt;p&gt;What's worth naming explicitly is &lt;em&gt;why&lt;/em&gt; this works as well as it does, and for as long as it does. A Corncob's obstruction is rarely framed as obstruction — it's framed as caution, rigor, "just asking questions," or insisting on a process step that conveniently never quite finishes. None of that is free: every round of relitigating a decision, every extra review gate, every "let's circle back" has a cost, paid by the people waiting on the decision. But that cost lands on &lt;em&gt;other&lt;/em&gt; people's timelines, not the Corncob's — which means the Corncob never receives the feedback signal that would tell them the obstruction has a price. It's the same mechanism as Design by Committee and Mushroom Management, just personalized: the person creating the friction is structurally insulated from feeling it, so the friction has no reason to stop.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing: Reinventing the Wheel, On Purpose
&lt;/h2&gt;

&lt;p&gt;There's one more entry from the original list worth ending on, because it inverts the usual direction of travel: &lt;strong&gt;Reinvent the Wheel&lt;/strong&gt;. In 1998, this was unambiguously a problem — a pervasive lack of technology transfer between projects meant teams kept rebuilding things that already existed elsewhere, at real cost in time, money, and risk.&lt;/p&gt;

&lt;p&gt;In 2026, with the sheer density of convenience frameworks available — frameworks that, as several of the chapters above describe, tend to arrive with their own lifecycle, their own conventions, their own lock-in, and their own accidental layering — "reinventing the wheel" sometimes means writing fifty lines of code that do exactly what you need, instead of pulling in a dependency that does that &lt;em&gt;and&lt;/em&gt; a hundred things you don't, each of which is now a thing your codebase is entangled with.&lt;/p&gt;

&lt;p&gt;There's a second, less obvious payoff. A wheel you build yourself — a rich domain model, built from first principles around your actual concepts (Risk Summaries and all the rest) rather than around a framework's idioms — tends to &lt;em&gt;survive&lt;/em&gt;. It can be carried forward across framework versions, across migrations, sometimes across entire platform changes, because it was never coupled to any of those things in the first place. The framework-shaped wheel, by contrast, often has to be substantially rebuilt with every major version bump, every "the framework now does this differently" release — which is the upgrade-treadmill cost from Chapter 1's Vendor Lock-In section, paid again and again. "Reinvent the wheel, once, properly" can be cheaper over a decade than "rent someone else's wheel, and rebuild your dependence on it every year or two."&lt;/p&gt;

&lt;p&gt;This isn't a blanket argument against frameworks, any more than the rest of this article is a blanket argument against microservices, CQRS, or Scrum. It's the same observation, one more time, from a different angle: every one of these "cures" was a legitimate answer to a real problem, in some context. The AntiPatterns above aren't lists of things to never do. They're the original, largely-forgotten warning labels — written before any of today's specific technologies existed, describing the &lt;em&gt;shapes&lt;/em&gt; of failure with enough precision that, almost thirty years later, you can hold the old description up against today's stack and watch it line up, name for name, almost too well.&lt;/p&gt;

&lt;p&gt;Nobody's selling the antidote. But the list was always right there.&lt;/p&gt;

</description>
      <category>java</category>
      <category>designpatterns</category>
      <category>softwaredevelopment</category>
      <category>software</category>
    </item>
    <item>
      <title>How To Prevent Contradicting AI Prompts</title>
      <dc:creator>Leon Pennings</dc:creator>
      <pubDate>Wed, 10 Jun 2026 07:00:21 +0000</pubDate>
      <link>https://dev.to/leonpennings/how-to-prevent-contradicting-ai-prompts-217a</link>
      <guid>https://dev.to/leonpennings/how-to-prevent-contradicting-ai-prompts-217a</guid>
      <description>&lt;h3&gt;
  
  
  You've Either Seen This Already, Or You Will
&lt;/h3&gt;

&lt;p&gt;You're building with AI. It's going well. Features appear quickly, the code is clean, the application works. You describe what you need, the AI implements it, you move on.&lt;/p&gt;

&lt;p&gt;Fifty prompts in, maybe a hundred, maybe two hundred — something breaks. Not dramatically. A behaviour that should be consistent isn't. A rule that was established early is being violated somewhere downstream. A customer finds an edge case that produces an answer that contradicts another part of the system.&lt;/p&gt;

&lt;p&gt;You dig in. The code at each location looks reasonable. Both implementations made sense when they were written. But they cannot both be right. Somewhere, somehow, the application has developed two incompatible beliefs about how something works.&lt;/p&gt;

&lt;p&gt;The immediate instinct is to fix the prompt. Be more explicit next time. More structured. More careful about context. Give the AI better instructions and this won't happen again.&lt;/p&gt;

&lt;p&gt;That instinct is wrong. And acting on it — more careful prompting, stricter templates, longer context windows — will delay the next contradiction but will not prevent it. Because the contradiction did not come from the prompting. It came from somewhere the prompting cannot reach.&lt;/p&gt;

&lt;p&gt;This article is about where it actually comes from. And about a solution that is older than AI, older than the frameworks that preceded it, and consistently buried by an industry that keeps rediscovering the same problem and forgetting the same answer.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Prompt Isn't The Problem
&lt;/h3&gt;

&lt;p&gt;Here is what the contradiction actually looks like.&lt;/p&gt;

&lt;p&gt;A B2B sales platform. Early in the build, prompt 75 establishes what an Order is: it belongs to a single customer, ships to a single delivery address, and is invoiced to a single billing contact. Clean, simple, the AI implements it correctly. Every subsequent prompt that touches Orders — discount calculation, delivery estimation, invoice generation, fulfilment tracking, customer notifications — is written on that assumption. None of those prompts are wrong. They are all consistent with the terrain as it was understood at the time.&lt;/p&gt;

&lt;p&gt;Eight months later, a different developer picks up a new requirement. Corporate customers need to split a single order across multiple departments, each with their own delivery address and cost centre. Prompt 235 asks for multi-address order support.&lt;/p&gt;

&lt;p&gt;The AI implements it correctly. Locally it is reasonable. But it has just redefined what an Order is — from a thing that belongs to one address to a thing that can belong to many. The terrain underneath has shifted. Every prompt written between 75 and 235 that touched delivery address, invoice recipient, or customer identity was built on ground that no longer exists.&lt;/p&gt;

&lt;p&gt;The developer writing prompt 235 does not know this. They were not there for prompt 75. Eight months is long enough for team composition to change, long enough for the original assumption to exist only in the memory of someone who may no longer be on the project. There is no artifact they could have consulted. The assumption was never written down. It was the water everyone was swimming in — until it wasn't.&lt;/p&gt;

&lt;p&gt;So where do you look? The AI wrote both implementations correctly. The prompts were both reasonable. There was no mistake at the point of instruction. The contradiction exists in the space between the prompts — in the overall model of what an Order actually is, which was assumed but never defined.&lt;/p&gt;

&lt;p&gt;And the cascade is not just these two prompts. It is every prompt in between. Reporting, discounting, fulfilment, notifications — all of it was written on the assumption of a single address. None of it is obviously broken. All of it is now wrong in ways that will only surface when a corporate customer places their first multi-department order.&lt;/p&gt;

&lt;p&gt;Better prompting cannot fix this. You cannot write a prompt that corrects a contradiction you do not know exists. You cannot ask the AI to be consistent with a model that was never articulated. The problem is not the quality of the instructions. &lt;strong&gt;The problem is the absence of something the instructions could be consistent with.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Rebuilding Doesn't Work
&lt;/h3&gt;

&lt;p&gt;The rebuild instinct is understandable. The application is a mess. The logic is scattered. Nobody knows where anything lives. Start over, do it right this time.&lt;/p&gt;

&lt;p&gt;But doing it right this time requires understanding the domain correctly this time. And the domain was not understood correctly before — not because the team was incompetent, but because understanding a domain correctly requires implementing it, adjusting it, hitting the contradictions, resolving them with the people who own the domain, and implementing again. That process takes time. It cannot be replaced by more careful planning.&lt;/p&gt;

&lt;p&gt;A rebuild without that process reconstructs the same misunderstandings into a cleaner codebase. The new system starts with higher accidental complexity — the lessons of the previous system encoded as defensive patterns — and the fundamental contradiction is still there, now buried deeper.&lt;/p&gt;

&lt;p&gt;This is not a failure of AI. This is the predictable result of building without a map. The AI is doing exactly what it is told. The problem is that what it is told has no center — no single coherent explicit model of the domain that all instructions must be consistent with. Without that center, contradictions are not just possible. They are inevitable. And no amount of rebuilding or re-prompting creates that center retroactively.&lt;/p&gt;

&lt;p&gt;The center has to come first.&lt;/p&gt;




&lt;h3&gt;
  
  
  What Fred Brooks Knew
&lt;/h3&gt;

&lt;p&gt;The center has to come first. Fred Brooks identified why, sixty years ago, and the industry has spent most of that time ignoring him.&lt;/p&gt;

&lt;p&gt;Brooks distinguished between two kinds of complexity in software. &lt;strong&gt;Essential complexity&lt;/strong&gt; is the complexity intrinsic to the problem itself — the business rules, the domain constraints, the lifecycle of an Order, the eligibility rules for a customer. It cannot be removed. It does not care what tools you use or what architecture you choose. The business is as complex as it is, and that complexity must be represented somewhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accidental complexity&lt;/strong&gt; is everything else. The frameworks, the indirections, the patterns applied without cause, the services that exist because nobody decided where the behaviour actually belonged. Accidental complexity is not intrinsic to the problem. It was introduced by the approach. And unlike essential complexity, it can be reduced — or avoided entirely.&lt;/p&gt;

&lt;p&gt;The distinction matters because it defines what is permanent and what is replaceable. The essential complexity of an application — correctly modelled — should outlast every framework it ever runs on, every infrastructure decision ever made about it, every team that ever works on it. It is the permanent part. Everything around it is the replaceable part.&lt;/p&gt;

&lt;p&gt;The problem the industry keeps having — with frameworks, with outsourcing, with AI — is that accidental complexity accumulates invisibly while essential complexity remains unmapped. The scaffolding grows. The domain shrinks. You end up with systems that are enormously complicated but that nobody truly understands, because the complication is in the support structure, not in the problem the system was built to solve.&lt;/p&gt;




&lt;h3&gt;
  
  
  Rivers and Terrain
&lt;/h3&gt;

&lt;p&gt;Requirements describe motion. A user does something, something happens, something else is notified. User stories are motion. Process diagrams are motion. Even event-driven architecture — at its conceptual heart — is motion wearing a technical hat. The entire tradition of software specification is built around describing flows.&lt;/p&gt;

&lt;p&gt;Flows are rivers. And rivers follow terrain.&lt;/p&gt;

&lt;p&gt;The river is not the landscape. It is what happens when water finds the landscape and takes the path of least resistance. Change the landscape and the river moves. The river is a consequence, not a cause. Model only the river and you have captured something real — but something that will change every time the underlying landscape shifts.&lt;/p&gt;

&lt;p&gt;Terrain is what things &lt;em&gt;are&lt;/em&gt;. A watershed. A valley. A ridge that separates two drainage systems. These don't change when the season changes or when a new road gets built nearby. They predate the rivers and they will outlast them.&lt;/p&gt;

&lt;p&gt;In software, the terrain is the domain. What an Order actually is. What it means for a customer to be eligible. What obligations a contract creates and what events discharge them. These things don't change because a new payment provider came along or because the fulfilment process got reorganised. The terrain outlasts the rivers by years — often by decades.&lt;/p&gt;

&lt;p&gt;Prompt 75 was a river. Prompt 235 was a river. Both made sense as rivers. They contradicted each other because there was no terrain underneath them — no shared model of what an Order actually is that both rivers had to flow through. Without the terrain, each river gets its own private geography. Eventually they meet and the water goes somewhere it was never supposed to go.&lt;/p&gt;

&lt;p&gt;The missing center is the terrain. The fix is to build the map before you build the rivers.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Domain Expert's River
&lt;/h3&gt;

&lt;p&gt;The natural response is: talk to the domain experts. Capture the requirements thoroughly. Understand the business before building. Let them define the terrain.&lt;/p&gt;

&lt;p&gt;This is right in intent and consistently wrong in execution — for a reason that matters enormously.&lt;/p&gt;

&lt;p&gt;Domain experts know their domain the way someone knows a city they grew up in. They can navigate it perfectly without being able to draw the map. They know what they do. They know how they do it. They have decades of accumulated practice and judgment. But they know it as motion — as rivers — because motion is how work presents itself. Nobody experiences their job as terrain. They experience it as things they do.&lt;/p&gt;

&lt;p&gt;There is a deeper problem. The domain expert's current implementation is already shaped by their tools. The spreadsheet that manages the process, the manual step that exists because the old system could not handle the edge case, the workaround that became standard practice so long ago that nobody remembers it was a workaround — these are all rivers. Rivers shaped by the banks that the tools imposed.&lt;/p&gt;

&lt;p&gt;When a business moves from spreadsheets to an application, the naive approach is to reproduce the spreadsheet process in code. The rivers are clearly visible, the domain expert can describe them precisely, the implementation matches. It works. And the technical limitations of the spreadsheet have been permanently encoded into software that has no such limitations.&lt;/p&gt;

&lt;p&gt;The constraint that created the workaround is gone. The workaround remains. Now it is load-bearing.&lt;/p&gt;

&lt;p&gt;The right conversation with a domain expert is not "how do you do this." It is "why does this need to happen." Not the process — the obligation. Not the river — the terrain feature the river is flowing around.&lt;/p&gt;

&lt;p&gt;That question is uncomfortable. It implies the current process might be unnecessary, or suboptimal, or a historical accident. Domain experts have professional identity invested in how they work. The why question asks them to step outside that identity and examine the ground beneath it. Many have never been asked to do that. Some discover, when asked, that the why is murkier than they expected — that two people on the same team have different answers, that the original reason for a rule was forgotten decades ago, that what seemed like policy is actually habit.&lt;/p&gt;

&lt;p&gt;The developer who can ask why — and persist through the discomfort until the terrain becomes visible — is doing the hardest and most valuable work in software development. It is not a technical skill. It is closer to archaeology.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Contextual Center
&lt;/h3&gt;

&lt;p&gt;When the terrain is mapped — when the domain is understood at the level of what things &lt;em&gt;are&lt;/em&gt; rather than what they &lt;em&gt;do&lt;/em&gt; — it becomes possible to build a contextual center.&lt;/p&gt;

&lt;p&gt;The contextual center is the domain model. Not a database schema. Not a service layer. Not a collection of DTOs. The living, honest encoding of what the domain actually is — its entities, their invariants, their obligations, their lifecycles — expressed in code that a domain expert could read and recognise.&lt;/p&gt;

&lt;p&gt;When an Order knows what it means to be cancelled — not as a service method called from somewhere, but as behaviour that belongs to Order because cancellation is something that happens to Orders — the contextual center is doing its job. The logic is findable. It is in one place. A new developer can locate it. A domain expert can verify it. A compliance requirement can be checked against it.&lt;/p&gt;

&lt;p&gt;And contradictions become immediately visible. If prompt 235 contradicts prompt 75, the contradiction surfaces the moment you try to encode both in the same place. The Order cannot simultaneously honour two incompatible rules about what it is. The terrain model forces the question that the river implementations never asked.&lt;/p&gt;

&lt;p&gt;This is the fix for the contradicting prompt problem. Not better AI. Not more careful prompting. Not an agent that scans for logical inconsistencies. A contextual center that makes contradictions structurally impossible to hide.&lt;/p&gt;

&lt;p&gt;The contextual center also provides the simplicity test. If the domain model is honest — if it correctly reflects the terrain — then implementing a new river should be simple. The new requirement finds its place in something that already exists, or reveals through the friction of not fitting that the model needs to grow. Either outcome deepens understanding. Either outcome improves the system.&lt;/p&gt;

&lt;p&gt;If the implementation is getting complicated, the terrain is wrong. The complexity is not a problem to be solved with more framework or more abstraction. It is a signal. The domain is pushing back. Something in the model does not match something in reality, and the code is showing you where.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complexity is the symptom. Simplicity is the proof.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  The Scale Problem
&lt;/h3&gt;

&lt;p&gt;Here is where the industry is currently making its most expensive mistake.&lt;/p&gt;

&lt;p&gt;AI works. On small applications, on prototypes, on systems with a limited number of domain objects and a shallow set of business rules, AI-assisted development is genuinely fast and the results are genuinely clean. A developer can build a working application in two days that would have taken two weeks before. That is real. It is not marketing.&lt;/p&gt;

&lt;p&gt;The problem is that this success is being treated as proof that the approach scales.&lt;/p&gt;

&lt;p&gt;It does not. And the reason it does not is precisely the terrain problem.&lt;/p&gt;

&lt;p&gt;On a sufficiently small system, a skilled developer can hold the entire terrain in their head informally. No explicit model is needed because the model exists as intuition. The contradictions surface quickly because the whole system is visible at once. The developer notices when prompt 235 conflicts with prompt 75 because they remember prompt 75. The cognitive map is small enough to carry.&lt;/p&gt;

&lt;p&gt;Past the point where that informal map breaks down, everything changes. The developer can no longer hold all of it. The contradictions stop surfacing naturally and start accumulating silently. Each new feature lands in a system that is slightly less understood than it was before. The AI keeps implementing faithfully. The terrain keeps drifting from the model nobody wrote down.&lt;/p&gt;

&lt;p&gt;This is the same reason waterfall worked on small projects and failed on large ones. Small projects could be designed upfront because the designer could hold the full domain in their head. Large projects could not because the domain was too complex to fully understand before implementation began. The implementation friction — the discovery process — was not optional on large systems. It was the mechanism by which the design became correct.&lt;/p&gt;

&lt;p&gt;The scale threshold is also closer than most teams expect — and AI makes it arrive faster. A real business domain hits the limits of informal terrain mapping sooner than it appears, and AI compresses that timeline further. What took months of traditional development now takes weeks of AI-assisted development. The cognitive collapse happens before anyone realises they are out of their depth. The prototype that took two days felt manageable. The enterprise system that grew from it in two months does not.&lt;/p&gt;

&lt;p&gt;A prototype that works is not proof that the architecture scales. It is proof that the architecture works at prototype scale. These are different things, and confusing them is one of the most consistent and expensive mistakes in software development.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why The Feedback Loop Cannot Be Outsourced
&lt;/h3&gt;

&lt;p&gt;If the terrain needs to be mapped, and domain experts know the terrain, why not map it thoroughly upfront and then implement? Design the domain model first, hand it to AI, let AI build the rivers.&lt;/p&gt;

&lt;p&gt;This is waterfall. And the industry already learned — expensively — why it does not work on complex domains.&lt;/p&gt;

&lt;p&gt;Waterfall failed not because the process was badly designed. It failed because its founding assumption was wrong. You cannot fully know a complex domain before you implement it. The implementation is part of how you come to know it.&lt;/p&gt;

&lt;p&gt;Code is the only medium that does not permit vagueness. A conversation can agree on a concept while each participant imagines something different. A document can describe a process while leaving its edge cases undefined. Code cannot. When you try to implement something ambiguous, the ambiguity surfaces. The implementation forces the question. That forcing is not a bug in the process. It is the mechanism by which the terrain gets mapped.&lt;/p&gt;

&lt;p&gt;Agile's real insight — the one that got buried under standups and story points and velocity metrics — was never about delivery speed. It was about shortening the feedback loop between building and learning. The two-week sprint is not valuable because it ships faster. It is valuable because it forces a confrontation with reality every two weeks. Assumptions get tested. Misunderstandings surface. The terrain model gets corrected before it drifts too far from the domain.&lt;/p&gt;

&lt;p&gt;Agile slowed down to learn faster. Each sprint is a correction cycle. The terrain is never assumed to be known — it is continuously refined through the friction of implementation.&lt;/p&gt;

&lt;p&gt;Now "AI makes waterfall possible again" is being said as though it is a good thing. As though the problem with waterfall was implementation speed. It was not. The problem was the learning gap — the distance between assumption and correction. AI does not close that gap. It widens it. You design upfront, AI implements the full design in days, and the contradictions are baked in at scale before a single domain expert has seen the system running.&lt;/p&gt;

&lt;p&gt;The implementation friction is not waste. It is the curriculum. Remove it and you have output without comprehension. Rivers without terrain. Working software that nobody truly understands, built at a speed that makes the misunderstanding very expensive to correct.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Outsourcing Lesson
&lt;/h3&gt;

&lt;p&gt;This specific mistake — removing the implementation friction in pursuit of cheaper, faster output — has been made before. Recently enough that people who lived through it are still working.&lt;/p&gt;

&lt;p&gt;In the first outsourcing boom, the promise was cheaper implementation. Move the development work to lower-cost locations. The rivers would still get built. The application would still ship. Why pay more for the same output?&lt;/p&gt;

&lt;p&gt;It worked — in the same way that building rivers without terrain works. The applications shipped. The initial costs were lower. And then the invisible invoice arrived.&lt;/p&gt;

&lt;p&gt;Because the friction disappeared. The developer working from a specification document in a different building, in a different timezone, had no access to the terrain discovery process. They implemented what was written. What was written was a river. The why never made the journey — not because anyone was careless, but because the why was not in the document. It was in the conversation, in the hallway, in the moment a developer overhears a domain expert explaining something to a colleague and realises the mental model in the code is wrong.&lt;/p&gt;

&lt;p&gt;The industry learned — expensively — that proximity was not a preference. It was the mechanism. The daily friction of shared space and shared context, being present when the domain expert says something offhand that rewrites your understanding of the terrain, cannot be async. It cannot be documented. It cannot be specified in a ticket.&lt;/p&gt;

&lt;p&gt;The correction was to bring development back. Not for cultural reasons. Not for communication style. To keep the learning loop intact.&lt;/p&gt;

&lt;p&gt;The lesson was learned. Then it was forgotten. Because it was never written down as a principle. It was attributed to communication problems, to cultural differences, to time zone friction. The real cause — that implementation is a learning process and learning cannot be outsourced — was never stated clearly enough to survive as institutional knowledge.&lt;/p&gt;

&lt;p&gt;"Get onboard with AI or get left behind" is the same sentence as "outsource or get left behind." Same promise. Same mechanism. Same blind spot. Same invoice, on its way.&lt;/p&gt;




&lt;h3&gt;
  
  
  Unfalsifiability, Again
&lt;/h3&gt;

&lt;p&gt;Why does this keep happening?&lt;/p&gt;

&lt;p&gt;Because working software is unfalsifiable as a measure of quality. The application that shipped — built with rivers and no terrain — is always beating the hypothetical application built with a domain model first. The delivered system always beats the unbuilt better one. There is no comparison. The invisible invoice has no line items. The cost shows up as enterprise complexity, as technical debt, as that is just how large systems work — and it is never traced back to the decision to build rivers without mapping the terrain.&lt;/p&gt;

&lt;p&gt;This is how the outsourcing lesson got forgotten. The costs arrived years after the decisions. By then the teams had changed. The attribution was impossible.&lt;/p&gt;

&lt;p&gt;This is how frameworks became permanent. Spring, CQRS, microservices, event-driven architecture — each one took a real problem and encoded a solution into a methodology. Each introduced accidental complexity that was invisible against the essential complexity it was supposed to manage. Each generated costs that arrived too late and too diffusely to be attributed. Each got adopted more widely because it was working — at the moment of evaluation, the only moment that counted. The pattern became the answer. The practice it was meant to serve got lost inside it.&lt;/p&gt;

&lt;p&gt;Domain-Driven Design followed the same path. Its early emphasis on shared language and rich domain models — the genuinely useful insight — gradually became overshadowed by discussions about bounded contexts, repositories, service layers, and event-driven decomposition. The vocabulary survived. The underlying purpose largely did not. Teams learned to say domain model while building something that looked like a domain model from the outside and functioned as a collection of data structures with behaviour scattered across service classes. The industry did to DDD what it does to everything else: turned a way of understanding reality into a collection of implementation patterns.&lt;/p&gt;

&lt;p&gt;And this is how AI will follow the same path. The small application works. The prototype is clean. The approach is validated — at the scale where informal terrain maps are sufficient, at the scale where the developer can hold it all in their head. The success is real. And it proves nothing about what happens at the scale where it matters.&lt;/p&gt;

&lt;p&gt;Unfalsifiability will do the rest.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Career Ceiling Nobody Discusses
&lt;/h3&gt;

&lt;p&gt;Junior developers learn rivers. That is where everyone starts, and it is the right place to start. Rivers are visible, implementable, testable. You can see when they work.&lt;/p&gt;

&lt;p&gt;Medior developers begin to notice that rivers have shapes — that some implementations feel natural and others feel like fighting the problem. This is the first intimation of terrain. The friction is trying to teach something.&lt;/p&gt;

&lt;p&gt;Senior developers think in terrain first. They talk to domain experts and hear why rather than how. They implement rivers to test terrain hypotheses and adjust when the implementation pushes back. They read complexity as a diagnostic signal rather than a problem to be solved with more pattern.&lt;/p&gt;

&lt;p&gt;The step from medior to senior is the step from river-thinking to terrain-thinking. And it is a step that frameworks and patterns have systematically prevented — not because the developers using them lack capability, but because the tools never forced the question. The framework absorbed the friction that would have taught it. The accidental complexity had somewhere to hide. The essential complexity stayed unmapped. The developer got faster at applying patterns, not better at questioning them. The work never demanded more, so more was never developed.&lt;/p&gt;

&lt;p&gt;This is not an indictment. It is a description of a system that produced exactly what it was designed to produce. The market said learn the framework, get the job. The framework said here is the structure, fill it in. The application shipped. Unfalsifiability validated everything. The question of whether there was terrain underneath never arose because it never had to.&lt;/p&gt;

&lt;p&gt;A significant proportion of working developers entered the field through routes — bootcamps, self-teaching, career changes — that are entirely oriented around framework fluency because that is what gets you hired quickly. That is a rational response to market incentives, not a character flaw. But it means the dominant population of working developers has been optimised for exactly the skill AI is now making unnecessary.&lt;/p&gt;

&lt;p&gt;AI does not eliminate these developers. It transforms them into AI operators. The framework templates get replaced by prompts. The pattern application gets replaced by merge request reviews. The output looks similar. The speed increases. And the bar lowers further, because prompting requires even less structural understanding than filling in a framework template did.&lt;/p&gt;

&lt;p&gt;What does not change is the invoice. The AI operator builds the same rivers faster, accumulates the same terrain debt faster, and hits the same ceiling faster. The application is cheaper to start and more expensive to maintain — the same curve as always, now compressed. And unfalsifiability protects the transition just as it protected everything before it. The framework developer becomes the AI operator and nothing looks different until the cascade arrives.&lt;/p&gt;




&lt;h3&gt;
  
  
  How AI Should Actually Be Used
&lt;/h3&gt;

&lt;p&gt;For small applications, AI as primary implementor is fine. The scale section explains why — the terrain is shallow enough to hold informally, the contradictions surface quickly, the cognitive map fits in one head. There is no problem to solve at that scale that AI creates.&lt;/p&gt;

&lt;p&gt;The problem starts when the application grows, or when the development team grows. Past the point where informal terrain maps break down, AI as primary implementor becomes the mechanism by which contradictions accumulate invisibly. Not because AI is the wrong tool — because the approach that worked at small scale does not transfer. Something has to change.&lt;/p&gt;

&lt;p&gt;What changes is how AI is used.&lt;/p&gt;

&lt;p&gt;AI is a pattern matcher with a vast, structured lexicon — and crucially, with understanding of what that lexicon contains. It has processed everything written about software, technology, architecture, and domains. That is not nothing. That is a remarkable instrument, if you use it for what it actually is.&lt;/p&gt;

&lt;p&gt;What it cannot do is discover terrain. A domain expert's specific business, with its specific history and specific constraints and specific why — that terrain has never been written down anywhere AI was trained on. It exists in conversation, in friction, in implementation. AI has no access to it. The developer is the only instrument that can pick it up.&lt;/p&gt;

&lt;p&gt;Which means AI and the developer are genuinely complementary. AI works on the known. The developer works on the specific. They operate on completely different material.&lt;/p&gt;

&lt;p&gt;As a discussion partner AI is genuinely useful — thinking out loud, testing an argument, asking what happens if a particular assumption is wrong. Not as a modeller, not as a designer. The conversation is the value. The understanding stays with the developer.&lt;/p&gt;

&lt;p&gt;As a technology consultant it earns its place completely. How does this technology work? What are the tradeoffs? How is this done in Java? These are questions AI answers well precisely because they are pattern questions — answered from a lexicon of everything written on the subject. The developer takes that knowledge and decides what it means for the domain model. That decision is never delegated.&lt;/p&gt;

&lt;p&gt;The code is written by the developer. Always. Because the act of writing it is the act of learning. The friction of making something work is how the terrain model gets validated. Outsource that friction and you outsource the understanding.&lt;/p&gt;

&lt;p&gt;Used this way, AI does not prevent learning. It removes the noise that would otherwise slow it down. The technology questions that used to cost an afternoon now cost ten minutes. Those minutes go back into the terrain work. The friction that was just overhead is gone. The friction that actually teaches something is preserved. The learning does not stop — it accelerates.&lt;/p&gt;




&lt;h3&gt;
  
  
  Two Approaches, Two Invoices
&lt;/h3&gt;

&lt;p&gt;AI does not level the playing field between the terrain approach and the river approach. It widens the gap between them.&lt;/p&gt;

&lt;p&gt;The AI operator — prompting rivers into existence without a contextual center — builds faster than a framework developer ever could. The initial output is impressive. The application ships quickly. But the terrain debt accumulates at the same rate as always, now compressed into a shorter timeline. The contradictions arrive sooner. The cascade of invalidated assumptions hits harder. The ceiling is the same ceiling. The invoice is the same invoice. It just arrives faster, with more confidence on the way there.&lt;/p&gt;

&lt;p&gt;The terrain mapper uses AI differently. Not as a primary implementor but as a mirror, a feedback loop, and a technology consultant. The discovery process still happens. The domain expert conversations still happen. The why questions still get asked. The contextual center still gets built. But the iteration cycles are faster, the edge case surfacing is faster, the technology decisions are faster. AI compresses the learning without bypassing it.&lt;/p&gt;

&lt;p&gt;This means the cost curve that was already cheaper in the long run gets cheaper in the short run too. The terrain mapper moves faster than before without accumulating the debt that was previously the price of moving fast.&lt;/p&gt;

&lt;p&gt;From the outside, at month two, the two approaches look identical. Both are shipping quickly. Both are producing working software. Unfalsifiability does its work. Nobody sees the difference until the contradictions start surfacing — by which point the AI operator is already describing it as enterprise complexity and looking for a pattern to absorb it.&lt;/p&gt;

&lt;p&gt;The industry is measuring AI's value in speed. Speed is real. But speed applied to the wrong approach does not reduce cost. It compresses the timeline to the invoice. The question was never how fast you can build rivers. It was always whether the terrain underneath them is honest.&lt;/p&gt;

&lt;p&gt;AI makes the right approach faster. It makes the wrong approach faster too. The difference is what you are left with when the speed runs out.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Solution Is Thirty Years Old
&lt;/h3&gt;

&lt;p&gt;There is no new methodology needed here. The problem is real and urgent and the answer has been available for decades — practised long before it acquired a name, and largely buried since it did.&lt;/p&gt;

&lt;p&gt;Build a domain model. Not a framework-prescribed structure, not a pattern applied because the textbook recommends it — an honest, simple encoding of what the domain actually is. Make it the contextual center of the application. Keep it simple enough that a domain expert can read it and recognise it. Keep it simple enough that complexity registers as a signal when it appears.&lt;/p&gt;

&lt;p&gt;Talk to domain experts about why, not how. Push through the river they offer you to the terrain underneath. Distinguish what the business requires from what the spreadsheet required. Implement rivers one at a time, learning the terrain as you go. Adjust the model as understanding deepens — because understanding will deepen, because it never stops deepening, and because that is the point.&lt;/p&gt;

&lt;p&gt;Build the shared vocabulary between the development team and the domain experts so the words in the code mean the same thing as the words in the business. Not because naming is important for aesthetic reasons, but because shared language is how you know you are mapping the same terrain. When a developer and a domain expert use the same word and mean different things, the terrain model is wrong. The language makes that visible before the code does.&lt;/p&gt;

&lt;p&gt;Accept that the first map is wrong. It will be. That is not a failure of the approach — it is the approach working. The map gets corrected through implementation. Each river teaches you something. Each correction makes the next river easier. The terrain model should get more true over time, not more obscure. That is the measure of whether the process is working.&lt;/p&gt;

&lt;p&gt;The contradiction between prompt 75 and prompt 235 is the same contradiction that lived in the fat service class, in the three microservices with incompatible Order logic, in the spreadsheet workaround encoded into the application. Different tools, different eras, same missing center.&lt;/p&gt;

&lt;p&gt;The center was always the answer. It still is.&lt;/p&gt;

&lt;p&gt;Build the map before you build the river. The rivers will be faster for it, and they will still be running in fifteen years.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is a follow-up to The Invisible Invoice: The Cost of Building Software Without Understanding It.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>softwaredevelopment</category>
      <category>ai</category>
      <category>architecture</category>
      <category>java</category>
    </item>
    <item>
      <title>The Invisible Invoice: The Cost of Building Software Without Understanding It</title>
      <dc:creator>Leon Pennings</dc:creator>
      <pubDate>Sun, 07 Jun 2026 18:13:39 +0000</pubDate>
      <link>https://dev.to/leonpennings/the-invisible-invoice-the-cost-of-building-software-without-understanding-it-4c25</link>
      <guid>https://dev.to/leonpennings/the-invisible-invoice-the-cost-of-building-software-without-understanding-it-4c25</guid>
      <description>&lt;h2&gt;
  
  
  The Wrong Measure
&lt;/h2&gt;

&lt;p&gt;Software doesn't fail when it stops working. It fails when the cost of keeping it working exceeds what anyone is willing to pay.&lt;/p&gt;

&lt;p&gt;That distinction sounds simple. Its consequences are not.&lt;/p&gt;

&lt;p&gt;The industry measures software by whether it works. Delivered on time, passes the tests, satisfies the requirements — success. The team moves on. The architecture gets praised, the approach gets repeated, the pattern gets adopted elsewhere. Nobody measures the cost of keeping it working six months later, three years later, after two team changes and four rounds of new requirements. That cost exists. It is often large. It is almost never attributed to the decisions that caused it.&lt;/p&gt;

&lt;p&gt;This is the central problem of software development, and it has a name: unfalsifiability. There is no comparable version of the same application, built differently, to measure against. The messy system that shipped is always beating the elegant system that wasn't built. The working application is always beating the hypothetical better one. You cannot prove a different approach would have been cheaper, because that approach was never taken.&lt;/p&gt;

&lt;p&gt;If it works, it's a success. And that is where all the trouble starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  You Don't Pay for Complexity When You Build It
&lt;/h2&gt;

&lt;p&gt;You pay for it every day afterward — and the invoice arrives without a line-item explanation.&lt;/p&gt;

&lt;p&gt;A framework that saves three weeks of initial development might cost three weeks a year in version upgrades, security patches, and breaking changes. Spread across five years, across a team of six, that initial saving is long gone. But nobody connects those upgrade sprints to the original decision to adopt the framework. The cost is real. The attribution is absent.&lt;/p&gt;

&lt;p&gt;This is what unfalsifiability does to cost. It doesn't make costs disappear — it makes them untraceable. The expense shows up as "enterprise complexity," as "technical debt," as "that's just how large systems work." It is rarely traced back to an architectural decision made three years ago by people who are no longer on the team.&lt;/p&gt;

&lt;p&gt;Consider what is actually being counted when someone says "we chose this framework to move faster." They are counting the lines of code they wrote. They are not counting the lines of code they are now responsible for — the framework itself. Those lines execute. They have bugs. They have CVEs. They have opinions about how your application should be structured, encoded in defaults and conventions that were answered before you understood your own domain.&lt;/p&gt;

&lt;p&gt;A domain-focused implementation without a heavyweight framework is typically smaller in raw lines of code than the template and configuration code required to set that framework up. Before a single line of business logic is written. Add the framework's own codebase — the code being executed on every request — and the surface area for bugs, security vulnerabilities, and maintenance burden has expanded by an order of magnitude. For what? For the privilege of not writing code yourself.&lt;/p&gt;

&lt;p&gt;A Ferrari is faster than a tractor on every measurable dimension. It is also completely useless in a field. And it costs more to buy, more to insure, more to service, and requires specialists to repair. Every dimension of cost is higher, for a vehicle that performs worse at the actual job. The sophistication is not the problem. The mismatch is the problem. And unfalsifiability means you never have to confront the mismatch directly — the Ferrari is technically moving across the mud, but you can't see how much damage it's doing to the soil, or how much you're spending on replacement clutches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know What You Are Doing
&lt;/h2&gt;

&lt;p&gt;Fred Brooks gave the industry a precise vocabulary for this problem fifty years ago, and the industry has largely ignored it.&lt;/p&gt;

&lt;p&gt;Essential complexity is the complexity intrinsic to the problem itself. It cannot be removed. It is the business rules, the domain constraints, the lifecycle of an order, the eligibility rules for a customer, the regulatory requirements of a financial product. This complexity exists whether you model it or not. The business is as complex as it is.&lt;/p&gt;

&lt;p&gt;Accidental complexity is everything else. The frameworks, the indirections, the patterns applied without cause, the services that exist because nobody decided where the behavior actually belonged. Accidental complexity is not intrinsic to the problem. It was introduced by the approach.&lt;/p&gt;

&lt;p&gt;The critical implication is this: you can only minimise what you can see. And in most software built today, the essential complexity is invisible — scattered across service classes, duplicated across microservices, buried under framework conventions — while the accidental complexity is everywhere and growing. The map is the problem, not the territory.&lt;/p&gt;

&lt;p&gt;Getting this right requires knowing what the application must do. Not what it does — what it must do. The mandatory behavior, the non-negotiable rules, the core of what this system exists to perform. Everything else is optional. Everything else has a cost. And that cost should be justified, explicitly, before it is paid.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tractor on the Field
&lt;/h2&gt;

&lt;p&gt;Simplicity is not an aesthetic preference. It is the engineering discipline of not paying for things you don't need.&lt;/p&gt;

&lt;p&gt;The simplest solution that honestly expresses what the application must do is not the lazy solution. It is the hardest solution to find, because it requires actually understanding the problem before reaching for the tools. It is also the solution that survives. Not because simple things are inherently more durable, but because simple things are easier to understand, easier to change, and easier to replace when understanding deepens.&lt;/p&gt;

&lt;p&gt;Implementation is a learning process. You do not know the domain fully when you begin. You discover it through building, through conversation with the people who own it, through the friction of encoding rules that turn out to be more nuanced than they first appeared. The application you build in month one is not the application the business needs in month eighteen. The question is whether you built something that can become that application, or something that has to be replaced by it.&lt;/p&gt;

&lt;p&gt;A minimalist approach — not sparse, not incomplete, but precisely sufficient — is the tractor on the field. Unglamorous. Fit for purpose. Still running in fifteen years. Serviceable by someone who wasn't there when it was built. Modifiable without calling a specialist. Cheap to operate on the days nothing goes wrong, and cheap to fix on the days something does.&lt;/p&gt;

&lt;p&gt;The Ferrari has its place. That place is not most software. And unfalsifiability means the Ferrari stays in the field long after it's clear it isn't working, because there's no other field to compare it to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the Essential Complexity Visible
&lt;/h2&gt;

&lt;p&gt;The domain model is not a goal. It is not a purity exercise. It is not an architectural pattern to be applied because the textbook recommends it.&lt;/p&gt;

&lt;p&gt;It is a tool for making the essential complexity of the application visible, centralized, and honest.&lt;/p&gt;

&lt;p&gt;When the business logic of an Order lives in the Order — when an Order knows what it means to be cancelled, what it means to be fulfilled, what state it must be in before shipment can proceed — that logic is findable. It is in one place. A new developer can locate it. A domain expert can read it and recognize it. A compliance auditor can verify it. When that same logic is scattered across service classes, duplicated in three microservices, and partially encoded in database triggers, it is effectively invisible. It exists. It executes. Nobody knows exactly where it is or whether the three copies agree with each other.&lt;/p&gt;

&lt;p&gt;The legibility bar matters here, and it should be set higher than most developers expect. A domain expert who is not a developer should be able to read the core domain objects and understand what the application is doing and why. Not the implementation details — the behavior. What is an Order? What can it do? What does the business enforce at that level? If the answer to those questions requires navigating framework annotations, service orchestrators, and DTO mappings, the essential complexity is not legible. It is hidden. And hidden complexity is expensive complexity, because it has to be rediscovered every time it needs to change.&lt;/p&gt;

&lt;p&gt;This is also where consistency becomes structural rather than aspirational. If everything Order-related happens in Order, then contradicting logic between two parts of the system is immediately obvious — because there is only one place to look. In a system where order logic lives in seventeen service methods across four microservices, contradiction is not just possible, it is inevitable. And nobody will notice until a customer finds it.&lt;/p&gt;

&lt;p&gt;The domain model is the centralized, codified, documented expression of what the business is. As long as the business continues in the same domain, that model should not have to be rewritten. The framework it runs on can be replaced. The delivery mechanism can change. The infrastructure can evolve. The essential complexity, correctly encoded, is the permanent part. Everything around it is the replaceable part. Getting that boundary right is the engineering challenge. Getting it wrong is what generates the invisible invoice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Domain Experts Close
&lt;/h2&gt;

&lt;p&gt;You cannot model what you do not understand. And you cannot understand a business domain from requirements documents, user stories, and ticket descriptions alone.&lt;/p&gt;

&lt;p&gt;Requirements describe motion through a system — a user does something, something happens. They teach you the rivers. A domain model teaches you the terrain. Without understanding the terrain, you are always following the water, never knowing where you are.&lt;/p&gt;

&lt;p&gt;Domain experts — the people who actually own the business processes, who know why the rules are the rules, who feel it when the software gets something wrong — are not stakeholders to be consulted at sprint reviews. They are the source of the essential complexity. The conversation with them is not a requirements-gathering exercise. It is the modeling work itself.&lt;/p&gt;

&lt;p&gt;The UI plays a specific role here. Not the polished end-user interface, but an early working interface that makes the domain model visible to the domain expert in a form they can evaluate directly. Two people can use the same word and mean different things. They can agree on a description and disagree entirely on what it describes. That misalignment is invisible in conversation. It is undeniable on a screen. Building something the domain expert can navigate is the fastest way to find out whether the model is honest.&lt;/p&gt;

&lt;p&gt;Implementation is a learning process. The model you have at the end of month one is not the model you will have at the end of year one. What you are building is not just software — it is accumulated understanding of what the business actually is. That understanding should be encoded in the model. The model should get more true over time, not more obscure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beware of the Hidden Costs
&lt;/h2&gt;

&lt;p&gt;Tools should be selected by one criterion: do they serve the essential complexity, or do they obscure it?&lt;/p&gt;

&lt;p&gt;A framework that handles persistence, wiring, or HTTP without imposing opinions about where behavior should live is earning its place. A framework that answers structural questions before you have understood your own domain — that substitutes a recipe for architectural thinking — is introducing accidental complexity from day one. You are paying for its opinions whether you wanted them or not.&lt;/p&gt;

&lt;p&gt;The distribution question deserves particular directness. Event-driven architecture, CQRS, and microservices each originated as responses to real problems at genuine scale. Each carries a significant and permanent cost: distributed tracing, eventual consistency management, versioned service contracts, deployment orchestration, network failure handling. And the loss of one guarantee that a single well-modeled application provides for free — transactional consistency. Once operations are distributed across services and event queues, rollback is no longer a database primitive. It is an engineering problem, solved with compensation logic and saga patterns, maintained indefinitely.&lt;/p&gt;

&lt;p&gt;There is also a structural cost that rarely gets discussed: distribution freezes your context boundaries. A monolith with a clear domain model can redraw its internal boundaries as understanding of the domain deepens — because learning continues, and the model should move as you learn. Once you have cut service boundaries and built contracts and deployment pipelines around them, that learning is frozen. Every misunderstanding about the domain that was encoded in a service boundary is now a permanent architectural feature. You pay for it in coordination overhead, in contract negotiation, in the impossibility of the refactoring that would have taken an afternoon in a monolith.&lt;/p&gt;

&lt;p&gt;These are expensive tradeoffs. They are justified at genuine scale — when one part of the system genuinely needs to scale independently, when teams are large enough that shared deployment is a bottleneck, when the operational investment is proportionate to the problem. For the vast majority of software, they are not justified. And unfalsifiability means they persist anyway, because the cheaper alternative was never built.&lt;/p&gt;

&lt;p&gt;The economic threshold is real. Distributed architectures make sense when you have hundreds of millions of rows of hot data, hundreds of thousands of concurrent users, and extreme load skew requiring parts of the system to scale independently by orders of magnitude. Most software never reaches that threshold. Most software pays the distribution tax anyway, and calls it modern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Longevity Is the Return on Investment
&lt;/h2&gt;

&lt;p&gt;Every argument in this article points to the same place, and the direction of travel is not what most people expect.&lt;/p&gt;

&lt;p&gt;When essential complexity is managed — made visible, centralized, and honest — ongoing costs drop. The model is the documentation, so documentation cannot go stale. The logic is in one place, so contradictions cannot accumulate quietly. New requirements find their place in something that already exists, or reveal through the friction of not fitting that the model needs to grow. Either outcome deepens understanding. Either outcome improves the system. Understanding compounds. The software gets easier to work with as it matures, not harder.&lt;/p&gt;

&lt;p&gt;When essential complexity is not managed, costs compound. Each new requirement lands on top of whatever was there before, in whatever shape it happened to be in. The essential complexity becomes harder to find, harder to verify, harder to change without touching something else. The team grows but delivery does not improve. The diagnosis is always the same: enterprise complexity, accumulated technical debt, that's just how large systems work. It is rarely diagnosed as what it actually is — the predictable consequence of building without a map.&lt;/p&gt;

&lt;p&gt;That is the ROI argument for managing essential complexity, and it is already strong. But there is a third level that the industry almost never discusses, because it inverts the assumption that rigor costs more upfront.&lt;/p&gt;

&lt;p&gt;When you understand the essential complexity of a system before you build it, initial development costs drop too.&lt;/p&gt;

&lt;p&gt;Not because the work becomes easier. Because you stop doing work that was never necessary. You select the tools the problem requires rather than the tools you already know. You do not adopt a framework whose opinions you will spend years working around, because you can see that those opinions do not fit your domain. You do not distribute a system that did not need to be distributed, because you can see that the transactional consistency you are about to give up is load-bearing. You do not build the service, the saga, the compensation logic, the versioned contract, the deployment pipeline — because you can see that the problem those things solve is a problem you created, not a problem you had.&lt;/p&gt;

&lt;p&gt;This produces two diverging cost curves that never cross — because the essential complexity approach was never the more expensive one. It only appeared that way because the costs of the alternative were invisible.&lt;/p&gt;

&lt;p&gt;The essential complexity approach starts lower — no unnecessary tooling, no framework opinions to work around, no infrastructure for problems you do not have. Each new function point finds its place in a model that already understands the domain. A new business rule is a method on the object that owns it. The cost per function point decreases over time as understanding compounds and the model absorbs requirements rather than accumulating them.&lt;/p&gt;

&lt;p&gt;The non-essential complexity approach starts higher — the framework, the boilerplate, the services, the distribution tax paid before a line of business logic exists. Each new function point adds new services, new mappings, new contracts. Logic that should live in one place gets duplicated across three. The cost per function point increases over time, because every addition lands in a codebase that is slightly harder to understand than it was before. The curve climbs until it hits a ceiling — the point where replacement is cheaper than continued maintenance or extension. At which point the system gets rebuilt. Without the domain model, the rebuild reconstructs the same misunderstandings into the new version, faster and with more confidence. The new system starts higher than the original did, climbs faster, and hits the ceiling sooner.&lt;/p&gt;

&lt;p&gt;The Ferrari does not just cost more to run. It costs more to buy. Understanding the domain first means you arrive at the dealership knowing you need a tractor — and you leave without the Ferrari, without its finance agreement, and without the specialist on retainer for the day it breaks down.&lt;/p&gt;

&lt;p&gt;Working software is not the asset. The understanding encoded in the software is the asset. A working application without that understanding is a disposable item — it worked when it left the factory, and it was not designed to be serviced.&lt;/p&gt;

&lt;p&gt;The tractor is still in the field in year fifteen. The Ferrari is in the shop, waiting for a specialist who knows the model. The tractor cost less on day one, costs less every year, and is still doing the job it was bought to do.&lt;/p&gt;

&lt;p&gt;The industry treats rigor as the expensive path. It is the only path that gets cheaper as you walk it.&lt;/p&gt;

&lt;p&gt;The invisible invoice arrives eventually. The only question is whether you chose a vehicle designed for the field — or whether someone is still trying to explain why the clutch keeps burning out.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Note From My Personal Experience
&lt;/h2&gt;

&lt;p&gt;The two cost curves described in this article are not theoretical. I have worked on and maintained two large systems over fifteen and seven years respectively. In both cases the core domain model has never needed to be rewritten. On the second system, the UI was replaced entirely — a complete rebuild — without the domain module being opened. The permanent part stayed permanent. The replaceable part was replaced. Exactly as intended.&lt;/p&gt;

&lt;p&gt;I have also introduced this approach into stalled projects at large organizations — systems where delivery had slowed, complexity had accumulated, and nobody could confidently explain where the business logic lived. In each case, making the essential complexity visible and centralized was what unstalled them. Not a new framework. Not a new architecture. Understanding what the system was actually for, encoded in a place everyone could find.&lt;/p&gt;

&lt;p&gt;The approach works. It has worked for fifteen years on one system, seven on another, and across multiple recoveries of projects that had lost their way. The cost curve is real. The only question is which one you want to be on.&lt;/p&gt;

</description>
      <category>softwaredevelopment</category>
      <category>architecture</category>
      <category>techdebt</category>
      <category>java</category>
    </item>
    <item>
      <title>AI and Enterprise Software Development</title>
      <dc:creator>Leon Pennings</dc:creator>
      <pubDate>Wed, 03 Jun 2026 08:01:53 +0000</pubDate>
      <link>https://dev.to/leonpennings/ai-and-enterprise-software-development-1611</link>
      <guid>https://dev.to/leonpennings/ai-and-enterprise-software-development-1611</guid>
      <description>&lt;p&gt;AI is the most significant shift in software development since the internet. Not because it changes what software can do — but because it accelerates the consequences of a distinction the industry has been treating as a preference for thirty years.&lt;/p&gt;

&lt;p&gt;Some software needs to work today. Some software needs to keep working — correctly, maintainably, through changing requirements and changing teams — for ten or fifteen years. These are not the same engineering problem. They never were. The tools and practices that serve the first actively undermine the second. AI does the first faster and better than any human developer ever has. What it does to the second is the subject of this article.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Enterprise Software Actually Is
&lt;/h2&gt;

&lt;p&gt;Enterprise software is not defined by its size, its industry, or its technology stack. It is defined by its relationship with time.&lt;/p&gt;

&lt;p&gt;An enterprise application must be correct today. It must remain correct as the business domain evolves around it. It must survive the teams that built it. It must adapt to requirements that nobody could fully predict when it was written. Implementation is not the primary challenge. Understanding — correct, durable, continuously updated understanding of the business domain — is the primary challenge. Implementation follows from that, and is the smaller part of the work.&lt;/p&gt;

&lt;p&gt;This is why the domain model is more important than the working application.&lt;/p&gt;

&lt;p&gt;That statement will make most developers uncomfortable, and it should. The working application is the visible artifact — the thing that gets demonstrated, delivered, and measured. But a working application without a domain model is a disposable item. It works when it leaves the factory. It was not designed to be serviced. When the business changes around it — and it will — the economics of repair exceed the economics of replacement. Except you cannot simply replace it, because without the model, you rebuild the same misunderstandings into the new version, faster, with more confidence.&lt;/p&gt;

&lt;p&gt;A domain model without a working application, on the other hand, is a foundation. Getting it working from that foundation is the smaller problem. It will stay working because the mechanism that keeps it correct is still present, still legible, and still honest.&lt;/p&gt;

&lt;p&gt;This distinction — between software built for today and software built to remain correct over time — was always there. It was always a structural choice with structural consequences. It was just treated as a preference, because the consequences were invisible. There was never a comparable version of the same application, built differently, to measure against. The cost of not modeling was permanently hidden.&lt;/p&gt;

&lt;p&gt;AI does not make that hiding impossible. The unfalsifiability remains intact — there is still no comparable application built the other way to measure against. What AI does is accelerate the accumulation of consequences, while making the codebase look cleaner than procedural development ever did. That combination is more dangerous than what came before, not less.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Invisible Breakdown
&lt;/h2&gt;

&lt;p&gt;Enterprise software has a failure mode that almost nobody correctly diagnoses, for a simple reason: diagnosing it requires a reference that doesn't exist.&lt;/p&gt;

&lt;p&gt;When an enterprise system becomes difficult to maintain — when features take longer than they should, when bugs touch more than they should, when the team grows but delivery doesn't improve — the diagnosis is almost always the same: this is what enterprise development looks like. Complex domain. Large codebase. Accumulated technical debt. The solution offered is more developers, more process, more tooling.&lt;/p&gt;

&lt;p&gt;The real diagnosis requires asking: what would this system look like if it had been built around a rich domain model from the start, maintained over the same period? That version was never built. The comparison is not available. So the decay gets attributed to enterprise complexity rather than to the absence of the structure that would have prevented it.&lt;/p&gt;

&lt;p&gt;What the industry misread as the natural difficulty of enterprise development is in most cases the consequence of a broken PDCA cycle. Plan, do, check, act. In enterprise software, the Check step requires being able to find what was encoded, verify it against current understanding, and update it. That requires the essential complexity of the system to be visible, owned, and in one place.&lt;/p&gt;

&lt;p&gt;Procedural development does not slow this cycle down. It breaks it. Each piece of logic that lives somewhere convenient rather than somewhere correct, each duplicated rule, each behavior scattered across service classes rather than owned by the concept it belongs to — each one removes a piece of the map the Check step needs. Eventually the map is gone. New requirements get added on top of existing ones without anyone being confident what the existing ones actually do. Contradictions accumulate. The system becomes a record of everything that was ever asked for, in chronological order, with no coherent structure underneath.&lt;/p&gt;

&lt;p&gt;This is not enterprise complexity. It is the consequence of building for today without encoding understanding in a form that survives tomorrow. And it was always going to happen — because procedural code has no mechanism for keeping the Check step alive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Practices That Keep the Cycle Running
&lt;/h2&gt;

&lt;p&gt;There are two practices that prevent this breakdown. They are not a methodology. They cannot be certified. They are disciplines — each with a precise job, sequential and mutually dependent.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The UI: Verifying the Ubiquitous Language
&lt;/h3&gt;

&lt;p&gt;The first practice is building a UI in the first month of any project — not for end users, not for customers, but as the primary instrument for verifying that the developer and the domain expert are actually talking about the same thing.&lt;/p&gt;

&lt;p&gt;This requires immediate clarification. The instinct on projects without obvious end-user interfaces — data pipelines, processing engines, integration layers — is to defer or skip the UI entirely. That instinct is wrong in a specific and consequential way. The UI is not a deliverable. It is a yardstick for the ubiquitous language — the shared vocabulary between developer and domain expert that the entire system depends on being correct.&lt;/p&gt;

&lt;p&gt;In twenty-five years of building software with business owners and functional application managers, the same sentence appears at nearly every meaningful discussion: &lt;em&gt;"It sounds correct, but I need to see it working."&lt;/em&gt; This is not a failure of imagination. It is an honest statement about the limits of language as a medium for domain transfer. Two people can use the same word and mean different things. They can agree on a description and disagree entirely on what it describes. That misalignment is invisible in conversation. It is undeniable on a screen.&lt;/p&gt;

&lt;p&gt;The UI forces concepts into a form the domain expert can evaluate directly. The concept the developer calls an &lt;em&gt;order&lt;/em&gt; and the business expert calls an &lt;em&gt;order&lt;/em&gt; either map to the same thing or they don't — and the screen is where you find out. The flow the developer modeled as a linear sequence and the business expert understands as a set of parallel states either match or they don't — and the screen is where you find out. No whiteboard session, no requirements document, no sprint review produces this verification with the same precision and immediacy as a working interface the domain expert can navigate directly.&lt;/p&gt;

&lt;p&gt;Conceptual thinking is genuinely scarce in software development. Developers are trained to implement described behaviour, not to reconstruct the mental models that produced the description. The UI compensates for this structurally. It makes the domain model visible and therefore falsifiable — which is the only condition under which a domain expert can tell you whether you understood them.&lt;/p&gt;

&lt;p&gt;It does not need to be polished. It needs to work, built in semantic HTML that will survive the project's lifetime without becoming a maintenance liability of its own. Its purpose is not presentation. It is verification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The UI is how you learn the domain correctly. It is the input to everything that follows.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Domain Model: Encoding That Learning Durably
&lt;/h3&gt;

&lt;p&gt;The second practice is encoding what you learned in a rich domain model — and this is where the oldest lesson in software development applies in its most consequential form.&lt;/p&gt;

&lt;p&gt;Keep what belongs together in the same place, so nobody has to explain where anything is.&lt;/p&gt;

&lt;p&gt;That is the difference between a thousand-piece puzzle and a twenty-five-piece puzzle. How orders are treated in the system can be found in the Order domain object. One place. Non-duplicated logic. Non-contradictory logic. A new developer, a new requirement, a compliance audit — all of them go to the same place and find the same answer.&lt;/p&gt;

&lt;p&gt;The domain model is a set of objects, each playing a defined role in the business domain, each owning the responsibility that role entails. Not data structures with methods bolted on. Objects that know what they are responsible for, enforce their own rules, and carry their own behavior. An Order that knows what it means to be cancelled. An Interaction that owns the transactional boundary — carrying the current user, the active roles, the deferred consequences that execute at its close. A KYC entity that owns the rules governing its own assessment.&lt;/p&gt;

&lt;p&gt;This is what keeps the PDCA cycle alive. The Check step can still reach what was Done — ten years later, after three teams, through changing requirements. The domain model is the map. As long as the map is honest and current, the cycle runs. New understanding finds its place. The model grows more true over time rather than more obscure.&lt;/p&gt;

&lt;p&gt;A consequence of this structure that is rarely discussed is what it provides for free. When the domain model owns its behavior and an Interaction owns the transactional boundary, a failed operation rolls back completely — the database change, the email that hadn't been sent, the downstream consequence that hadn't fired. JDBC transaction rollback is a primitive. Consistency is structural. There is no compensation logic to write, no saga pattern to implement, no consistency verification to run after the fact. The guarantee emerges from the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The domain model is how you keep doing the correct thing, indefinitely. It is not documentation about the system. It is the system, in its most honest and durable form.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Relationship Between the Two
&lt;/h2&gt;

&lt;p&gt;These two practices are sequential and mutually dependent in a precise way.&lt;/p&gt;

&lt;p&gt;The UI without the domain model produces correct understanding encoded incoherently. The domain expert confirmed the language. The developer understood the domain. And then scattered it across service classes in a way nobody can find or follow three years later. The understanding was correct and it decays anyway — into the codebase, across layers, through framework conventions — until the next developer cannot reconstruct it.&lt;/p&gt;

&lt;p&gt;The domain model without the UI produces a cohesive model of something that may be wrong. Elegant, traceable, internally consistent, and externally misaligned. The developer's interpretation was never verified against the person who actually knows.&lt;/p&gt;

&lt;p&gt;Together they form a self-correcting cycle. The UI surfaces what the domain expert actually means. The domain model encodes that meaning durably. The UI surfaces the encoded meaning back to the domain expert for verification. The cycle is self-sustaining — not just at the start but throughout the life of the application.&lt;/p&gt;

&lt;p&gt;This is also why you can survive on the domain model alone — a slightly wrong model is still fixable, because the wrongness is visible and locatable, and the PDCA cycle is still running — but you cannot survive on the UI alone. Correct understanding that was never durably encoded dies with the people who held it.&lt;/p&gt;

&lt;p&gt;One practice optimizes doing the correct thing and discovering what the correct thing is.&lt;/p&gt;

&lt;p&gt;The other documents that discovery in the most undistortable form possible — for today's team, for tomorrow's maintainers, for the requirements nobody has thought of yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Industry Built Instead
&lt;/h2&gt;

&lt;p&gt;Without these two practices, enterprise software does not fail silently. Teams feel the friction. The PDCA cycle breaks. And the industry, characteristically, built architectures to manage the symptoms.&lt;/p&gt;

&lt;p&gt;Before reaching for those architectures, one question is worth asking honestly: does this system genuinely require the same scale as the organizations that invented these patterns — or is the complexity being solved a complexity that was created by code written without a domain model?&lt;/p&gt;

&lt;p&gt;CQRS, event-driven architecture, microservices — each originated as a response to real problems at genuine scale. Each carries a significant integration tax: distributed tracing, eventual consistency management, versioned service contracts, deployment orchestration, network failure handling, and the permanent loss of the one guarantee a single domain model provides for free — transactional consistency. Once operations are distributed across services and event queues, rollback is no longer a database primitive. It becomes an engineering problem, solved with compensation logic and saga patterns, maintained indefinitely, on top of the original modeling problem that the architecture was never asked to fix.&lt;/p&gt;

&lt;p&gt;A rich domain model makes most of that complexity structurally unnecessary. Not by being clever — by keeping what belongs together in one place, so the system never generates the problems these architectures were designed to manage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frameworks and the Question They Answer Too Early
&lt;/h2&gt;

&lt;p&gt;The same logic applies to the framework ecosystem, with one distinction worth making precisely.&lt;/p&gt;

&lt;p&gt;Frameworks like Spring exist to provide implementation convenience for developers who should not need to understand the underlying mechanisms. That is a blunt description, but it is an accurate one. Spring wires things together so you don't have to understand the wiring. It provides a transaction model so you don't have to manage transactions. The value proposition is working software without deep understanding of what produces it.&lt;/p&gt;

&lt;p&gt;That value proposition has a structural cost. Spring doesn't just provide convenience — it answers structural questions before you've understood the domain. The controller-service-repository recipe is not a neutral scaffold. It is an answer to where behavior should live, given before the domain had a chance to answer that question itself. Engineers who learned Spring as their foundation did not learn to reason about structure — they learned to apply a structure that was handed to them. When the recipe always fits, the judgment to know when it doesn't is never developed.&lt;/p&gt;

&lt;p&gt;The relevant question for any framework is: does removing it force you to split essential complexity? If yes, the framework earns its place. If no, it is providing implementation convenience that increasingly AI can provide — without the framework's architectural opinions, without its version upgrade cycle, and without the recipe it substitutes for structural thinking.&lt;/p&gt;

&lt;p&gt;Hibernate passes that test. Without it, the domain object and its persistence representation become two separate things — a DTO, a populator, a translation layer that has to be maintained in sync with the domain object when it changes. Hibernate collapses that into the domain object itself. The annotations are honest declarations of what the object requires from persistence. The object loads as itself. The domain model remains the single source of truth. Hibernate serves the model's integrity rather than substituting for structural thinking.&lt;/p&gt;

&lt;p&gt;Spring fails that test. Removing Spring does not split essential complexity. It removes the recipe that was preventing essential complexity from being properly owned. AI can now provide the implementation capability Spring was providing — without the recipe.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI: The Same Mistake, At Greater Speed
&lt;/h2&gt;

&lt;p&gt;This is where the two threads of this article converge — because AI is not a separate story from the domain modeling story. It is the aspect of it that finally makes the stakes undeniable.&lt;/p&gt;

&lt;p&gt;AI makes the same mistakes procedural programmers make. It builds what is required today without preparing for tomorrow. It fills in the template, makes it work, ships the feature. The code is correct for the prompt. Whether it is correct for the system — whether it is consistent with what was built six months ago, whether it contradicts a rule established in a different part of the domain, whether it is encoding understanding that will survive the next requirement — these are questions AI cannot ask, because asking them requires a domain model to check against, and AI builds no such model.&lt;/p&gt;

&lt;p&gt;For a large category of software, this does not matter. Small applications, internal tools, prototypes, systems with bounded scope and limited lifespans — here, building for today is the correct approach. AI is close to the complete solution. The template gets filled. The application works. The contradictions are manageable because the scope is small enough to hold in working memory. There is no ten-year maintenance horizon. Disposable software benefits from disposable development, and AI is the best disposable development tool ever built.&lt;/p&gt;

&lt;p&gt;The schism appears at enterprise software — and it is the same schism that always existed between procedural development and domain-modeled development, now made visible by the speed at which AI can accumulate the consequences.&lt;/p&gt;

&lt;p&gt;A procedural programmer building a large system makes their implementation decisions in isolation. Each feature is added to whatever was there before, in whatever shape it happened to be in. Over time the contradictions accumulate. A business rule exists in three places and was updated in two. A concept that should be unified has drifted into five different representations. The PDCA cycle broke quietly, feature by feature, and the system became a record of everything that was ever asked for rather than a model of what the business actually is.&lt;/p&gt;

&lt;p&gt;AI does the same thing, at a velocity no human team could match. The contradiction between prompt 1 and prompt 78 is invisible in working software and produces no error, no warning, no friction. The code works. The Check step has no map to navigate. The cycle was broken before it started.&lt;/p&gt;

&lt;p&gt;The critical point — and it is worth stating precisely because the counter-argument will come — is that this is not a memory problem. Expanding context windows do not resolve it. An AI holding millions of tokens of procedural code in its context window can still generate a patch that introduces a subtle business contradiction, because the limitation is not how much the AI remembers. It is that the AI has no model of what the system &lt;em&gt;should be&lt;/em&gt; — no canonical truth to check against, no domain concept that owns the rule being contradicted, no structure that would make the inconsistency visible before it becomes a bug.&lt;/p&gt;

&lt;p&gt;Rebuilding does not help. Without a domain model as the canonical reference, the rebuild reconstructs from the same conversations, the same scattered understanding, the same implicit contradictions — and produces the same contradicting system, faster, with more confidence.&lt;/p&gt;

&lt;p&gt;There is an irony worth naming here. AI does not need Spring. It does not need CQRS, event-driven architecture, or microservices. Those frameworks and architectures exist largely as scaffolding for procedural developers navigating complexity they could not otherwise manage — ways of imposing structure on code that had none, or distributing a system too incoherent to reason about as a whole. AI navigates that complexity effortlessly. It can implement a clean monolith in plain Java without the framework overhead, without the service boundaries, without the integration tax.&lt;/p&gt;

&lt;p&gt;So AI arrives at something that looks architecturally healthier than what procedural development typically produced — and then exhibits exactly the same underlying problem, on a larger scale, with the symptoms that used to make the problem visible earlier now absent. At least with microservices the integration pain was visible and attributable. The seams showed. Teams felt the coordination overhead and knew something was wrong. The AI monolith looks coherent on the surface. The contradictions are woven through a large, working codebase with no map and no visible seams — and the unfalsifiability that kept the invisible breakdown invisible gets stronger, not weaker. There is still no comparable version built with a domain model to measure against. The decay will still be attributed to the scale of the codebase, or to the prompts not being precise enough, or to enterprise complexity. The measurement problem that hid the breakdown before AI arrived continues to hide it after. It just hides a larger problem, accumulated faster, in code that looks cleaner than anything procedural development ever produced.&lt;/p&gt;

&lt;p&gt;Where AI genuinely excels in enterprise development is as a technology consultant. How do I stream documents to an HTTP multipart post. Can I do this in Java, and if so, how. What is the correct behavior of this Hibernate mapping in this edge case. These are questions with objectively correct answers. AI finds them instantly. The framework knowledge a developer spent years accumulating is now available on demand, to anyone who can ask the right question. That is a real democratization of technical expertise.&lt;/p&gt;

&lt;p&gt;The reason AI cannot build a domain model is structural, and it goes deeper than context windows or model size. AI is a pattern matcher. It is trained on vast amounts of code and text that has already been written, and it produces output that matches the patterns of what it has seen. This is genuinely powerful for everything that is a pattern — technology implementation, framework usage, boilerplate, adapter code. These things have been written before, in recognizable forms, and AI finds them reliably.&lt;/p&gt;

&lt;p&gt;A domain model is not a pattern. It is a representation of something specific that does not exist anywhere in training data — because it has never existed before. The order lifecycle for this logistics company, the KYC rules for this financial institution, the ownership structure for this regulatory context — these are particular, not general. They must be discovered through conversation with the people who know the domain, verified through a UI that makes the concepts visible, and refined through the friction of encoding them in code that either fits the model or reveals where the model is incomplete. None of that discovery process is available to AI as input. It has text. The domain exists in the heads of domain experts and in the resistance of implementation against a model being built for the first time.&lt;/p&gt;

&lt;p&gt;This is why larger models and longer context windows do not resolve the limitation. The constraint is not how much AI can remember or process. It is that domain modeling is a discovery activity, and the thing being discovered has never been written down in the form AI would need to pattern-match against. A model trained on ten times more data is ten times better at pattern matching. It is not closer to domain discovery, because domain discovery is a different kind of activity entirely.&lt;/p&gt;

&lt;p&gt;And there is a second layer to the limitation that compounds the first. Even without a domain model, AI cannot notice that one is missing. The signal that a model is absent is friction — the requirement that does not fit, the concept that keeps appearing in three different places, the rule that cannot be placed without contradicting something else. These are the signals that teach a human developer where the structure is wrong. AI absorbs that friction. It finds somewhere convenient for the requirement, implements it, and moves to the next prompt. The code works. The structural problem is invisible. The PDCA cycle was broken before it started, and nothing in the process signals that this has happened.&lt;/p&gt;

&lt;p&gt;What AI cannot do is find the correct domain concept, assign it its responsibility, and place it honestly in the model. That requires understanding the domain — which requires the UI verification, the whiteboard session, and critically, the friction of implementation. The resistance a new requirement produces against an existing structure is not an inconvenience. It is the domain teaching you something. It is where the modeling judgment gets built. A developer who prompts their way through that resistance never receives the lesson. The code works. The understanding was never deepened. The model drifts from the domain silently, until the drift becomes structural and the consequences become expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI is the perfect tool for building software for today. Enterprise software requires building for tomorrow. That has always been the distinction. AI accelerates the consequences of ignoring it — invisibly, in code that works, with no signal that anything is wrong.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Means
&lt;/h2&gt;

&lt;p&gt;The working application is not the asset. The domain model is the asset.&lt;/p&gt;

&lt;p&gt;For software with limited lifespans, this distinction is irrelevant. Build fast, use AI, ship it, replace it when it stops serving its purpose. The disposable approach is correct for disposable software, and AI makes it better than it has ever been.&lt;/p&gt;

&lt;p&gt;For enterprise software — applications that must remain correct through changing requirements, changing teams, and changing understanding, over years and decades — the domain model is the mechanism that keeps the PDCA cycle alive. The UI is the mechanism that keeps the domain model honest. Together they produce something AI cannot: software that gets easier to understand as it matures, because the understanding encoded in it compounds rather than decays.&lt;/p&gt;

&lt;p&gt;The developer who builds that foundation and uses AI for implementation is extraordinarily powerful. The patterns, the boilerplate, the technology questions, the adapter code surrounding a well-modeled domain — AI handles all of it, precisely and quickly, in service of a structure the developer owns and understands. That combination is more capable than anything the industry has previously had available.&lt;/p&gt;

&lt;p&gt;The developer who uses AI to avoid building that foundation is producing disposable software at enterprise scale — and will discover, somewhere around prompt 78, that working software and correct software are not the same thing, and that the gap between them compounds with every prompt that had no model to check against.&lt;/p&gt;

&lt;p&gt;The distinction between procedural development and domain-modeled development was never a preference. It was always a structural choice with structural consequences. The measurement problem that kept those consequences invisible — the absence of the comparable application built the other way — is being resolved in real time, at speed, by teams discovering that the working application they built in six months is already contradicting itself, with no map to navigate back to coherence.&lt;/p&gt;

&lt;p&gt;AI did not create this problem. It inherited it from procedural development, and it runs it faster than any human team ever could.&lt;/p&gt;

&lt;p&gt;The practices that prevent it are the same ones that always prevented it. They are just, finally, undeniably necessary.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is part of a series on software engineering craft. Previous pieces examine the rich domain model as a discipline, the properties of enterprise software that lasts, how the software industry mistook its tools for its craft, and why Scrum works only when the people making decisions feel the outcomes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwaredevelopment</category>
      <category>java</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Scrum Works — But Only When the People Making Decisions Feel the Outcomes</title>
      <dc:creator>Leon Pennings</dc:creator>
      <pubDate>Mon, 01 Jun 2026 05:18:35 +0000</pubDate>
      <link>https://dev.to/leonpennings/scrum-works-but-only-when-the-people-making-decisions-feel-the-outcomes-1l07</link>
      <guid>https://dev.to/leonpennings/scrum-works-but-only-when-the-people-making-decisions-feel-the-outcomes-1l07</guid>
      <description>&lt;p&gt;There is a version of Scrum that serves the product. There is another version that serves the agile transformation process. They use the same vocabulary, run the same ceremonies, and produce very different outcomes.&lt;/p&gt;

&lt;p&gt;The first treats the sprint as a learning unit — a cycle of building, showing, and understanding, with the product as the permanent reference point and the business owner as a continuous presence in the work. The second treats the sprint as a reporting unit — a cycle of planning, delivering, and demonstrating, with velocity as the measure of success and the business owner as an end-of-sprint audience.&lt;/p&gt;

&lt;p&gt;Most organisations believe they are running the first. Most are running the second. The difference is not methodology. It is consequence. And once you see it, you cannot unsee it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Scrum Actually Came From
&lt;/h2&gt;

&lt;p&gt;In 1986, Hirotaka Takeuchi and Ikujiro Nonaka published a paper in the Harvard Business Review studying how companies like Honda, Canon, and Fuji-Xerox built complex products faster and better than their competitors. They weren't studying software. They were studying what happened when you gave a cross-functional team a difficult goal, genuine autonomy, and full accountability for the outcome.&lt;/p&gt;

&lt;p&gt;What they found was not a process. It was a consequence structure. The engineers at Honda were not following a framework. They were people who could not afford to be wrong — whose careers, reputations, and sense of craft were inseparable from whether the thing they built actually worked. The overlapping development phases, the self-organising teams, the continuous learning — these weren't designed. They were the natural behaviour of committed people given a hard problem and the freedom to solve it.&lt;/p&gt;

&lt;p&gt;Takeuchi and Nonaka called one of their six key characteristics "multilearning" — the idea that learning had to happen continuously, at every level, through direct contact with the problem. Not through documentation. Not through handoffs. Through people who understood the domain working alongside people who understood the craft, close enough that ignorance was immediately visible and immediately corrected.&lt;/p&gt;

&lt;p&gt;Jeff Sutherland and Ken Schwaber read that paper and recognised something important: software teams were failing catastrophically because they were running relay races when they should have been playing rugby. Waterfall's sequential handoffs — requirements to design to development to testing to deployment — introduced months of lag between a decision and its consequences. By the time you discovered the requirements were wrong, you had built on top of them for a year.&lt;/p&gt;

&lt;p&gt;Their insight was correct. Tight feedback loops beat long planning cycles. Short iterations beat big bang releases. Direct domain contact beats document-mediated specification. The Agile Manifesto that followed made the priority order explicit: individuals and interactions over processes and tools, working software over comprehensive documentation, customer collaboration over contract negotiation, responding to change over following a plan.&lt;/p&gt;

&lt;p&gt;The right side of those statements had value. It was just less important than the left — and the Manifesto said so explicitly.&lt;/p&gt;

&lt;p&gt;That priority order has since been completely inverted — not because Scrum is flawed, but because of what was added to it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pig and the Chicken
&lt;/h2&gt;

&lt;p&gt;Early Scrum folklore told a story about a pig and a chicken who decided to open a restaurant together. The chicken suggested calling it "Ham and Eggs." The pig declined. "For you," the pig said, "that's a contribution. For me, it's a commitment."&lt;/p&gt;

&lt;p&gt;The story was eventually removed from the official Scrum literature — perhaps it seemed uncharitable. But the principle it pointed at was exactly right, and its removal is itself a symptom of what went wrong.&lt;/p&gt;

&lt;p&gt;Scrum works when the people doing the work are pigs. Fully committed. Consequentially exposed. When the product is wrong, they feel it. When the process slows the team down, they feel it. When the business owner's problem goes unsolved, they feel it. Their skin is in the game and the game's feedback reaches their skin.&lt;/p&gt;

&lt;p&gt;Scrum degrades when the chickens accumulate.&lt;/p&gt;

&lt;p&gt;A chicken is not a bad person. A chicken is a structurally consequence-free participant — someone who carries authority over how the work happens without bearing the outcome of that authority. They contributed something. They cannot be committed, because the structure doesn't allow it. They will move to the next engagement, the next team, the next organisation. The team will live with the consequences of their recommendations.&lt;/p&gt;

&lt;p&gt;The best chickens know this about themselves. The strongest consultant scrum masters and agile coaches actively work to reduce their own authority — pushing consequence back onto the team, making themselves progressively less necessary, effectively working toward their own redundancy. That is a mark of genuine craft in a consequence-free role. But it is a character trait, and you cannot scale character. You cannot hire for it reliably across an organisation. You cannot depend on it as a structural guarantee.&lt;/p&gt;

&lt;p&gt;This distinction matters more than any ceremony, any role definition, or any version of the Scrum Guide. Because a team of pigs running imperfect Scrum will self-correct — the feedback is immediate, the incentive to fix problems is intrinsic, and the process will evolve toward what actually serves the work. A team with too many chickens running perfect Scrum will drift toward process performance — because the people with the most authority over the process are the ones least exposed to whether it serves the product.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Happened to Scrum
&lt;/h2&gt;

&lt;p&gt;Sutherland and Schwaber encoded a pig observation into a framework. That was always going to be difficult — you cannot certify skin in the game. But the framework pointed at the right things. Self-organising teams. Direct customer contact. The scrum master conceived as someone embedded in the team's work, responsible for removing impediments that blocked delivery — not as an external observer of the team's process.&lt;/p&gt;

&lt;p&gt;Then the industry arrived.&lt;/p&gt;

&lt;p&gt;Not maliciously. Structurally. Organisations running Scrum at scale needed coordination mechanisms. The coordination mechanisms needed owners. The owners needed titles. The titles became roles. The roles became certifications. The certifications became hiring criteria. And at each step, the distance between process authority and process consequence grew a little wider.&lt;/p&gt;

&lt;p&gt;The scrum master became a process coach. External to the team. Often shared across multiple teams. Measured on ceremony quality, team satisfaction scores, and adherence to the framework. Not on whether the product served the business. The sentence that captures the failure mode perfectly is one you will recognise if you have heard it: &lt;em&gt;"I'll fix it next week — I have two other teams to coach."&lt;/em&gt; That sentence is structurally impossible if the scrum master is inside the team's consequence. It is inevitable if they are outside it.&lt;/p&gt;

&lt;p&gt;The product owner — originally a role requiring genuine domain authority and business accountability — became a proxy. A translator sitting between the team and the real decision-maker, filtering business knowledge through the medium of user stories, insulating engineers from the domain rather than connecting them to it.&lt;/p&gt;

&lt;p&gt;The infrastructure team — the one Scrum was partly designed to dissolve into the cross-functional whole — re-emerged as the CI/CD team, the platform team, the DevOps function. Different name badge. Same dashboard. Same structural distance from whether the product actually worked for the people who needed it. The dashboard stays green. The pipeline runs. And then there is still plenty of time to spend on minesweeper.&lt;/p&gt;

&lt;p&gt;And with each new chicken added, consequence density fell. The feedback loops that should have corrected mistakes grew longer and more attenuated. The process filled the gap — providing the appearance of rigour in the absence of the reality it was substituting for.&lt;/p&gt;

&lt;p&gt;Scrum was a revolt against exactly this. It became exactly this.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Definition of Done Is a Symptom
&lt;/h2&gt;

&lt;p&gt;Nothing illustrates the problem more precisely than what happened to the Definition of Done.&lt;/p&gt;

&lt;p&gt;Most DoDs are social contracts: cucumber tests passing, peer review complete, product owner sign-off received. And those boxes answer a different question entirely — not "is this good?" but "whose fault is it if this is wrong?"&lt;/p&gt;

&lt;p&gt;If all those boxes are checked, the engineer cannot be blamed if the functionality turns out wrong. They followed the process. The responsibility for whether it was the right thing to build is distributed so thinly across roles and sign-offs that it evaporates entirely. Nobody failed. The process succeeded. The business owner got something they didn't need, delivered on time.&lt;/p&gt;

&lt;p&gt;The DoD is not a quality gate. It is a consequence substitute. It exists precisely because the people doing the work cannot feel directly whether it is right — because the business owner is not in the conversation, and because the consequence of being wrong has been spread so thinly across roles that nobody feels it acutely enough without a checklist to reach for.&lt;/p&gt;

&lt;p&gt;Now consider the alternative. The business owner is genuinely part of the team — not as a stakeholder who attends the demo, but as a continuous presence in the work. The engineer who hits something that doesn't fit the model talks to them that day. Not next sprint. Not at the demo. That day. The working software is shown informally, mid-sprint, as a thinking tool — not as a deliverable but as a question: is this what you meant? The answer shapes the next two days of work, not the next sprint's backlog.&lt;/p&gt;

&lt;p&gt;In that environment, what does the Definition of Done do? It documents what both parties already know, through a checklist that neither party needed to reach the answer. The DoD didn't produce the quality. The conversation did.&lt;/p&gt;

&lt;p&gt;This is the cleanest diagnostic available for whether Scrum is serving your product or substituting for it: how prescriptive does your Definition of Done need to be? The more you need it, the further the business owner is from the work. A heavily checkbox-driven DoD is not evidence of good process hygiene. It is a measure of the consequence gap — the distance between the people who build and the people who know whether what was built is right.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stories Are Not Tickets
&lt;/h2&gt;

&lt;p&gt;The consequence gap shows up everywhere once you know to look for it, but nowhere more clearly than in how user stories are written and treated.&lt;/p&gt;

&lt;p&gt;Alistair Cockburn, one of the authors of the Agile Manifesto, described the story card as a token for a conversation — a placeholder that represented a discussion yet to happen, not a specification already agreed. That is a precise and important idea. The card was never meant to replace the conversation. It was meant to prompt it.&lt;/p&gt;

&lt;p&gt;What happened instead is that the card became the deliverable. The story became the ticket. And the conversation — the one that would have revealed what the domain actually needed — never happened, because the ticket already contained the answer.&lt;/p&gt;

&lt;p&gt;A story written as a ticket describes how. "As an invoice clerk I want to export the invoice to PDF and email it to the customer." That is not a business need. That is a current business process, translated into acceptance criteria, handed to an engineer as a specification. The how has been decided before the what was understood.&lt;/p&gt;

&lt;p&gt;The invoice clerk doesn't think of it as a how. For them, that is simply how invoicing works — how it has always worked, how they were trained to think about it. The mental model of the domain and the mental model of the current implementation have merged into one thing. When you ask them what they need, they describe what they do. This is not a failure of articulation. It is the natural epistemology of someone who lives inside a domain.&lt;/p&gt;

&lt;p&gt;The engineer who receives "export to PDF and email" as a ticket implements export to PDF and email. The box is ticked. The DoD is met. And the actual business need — that the customer receives timely, accurate confirmation of what they owe — remains unexamined. Maybe PDF email is the right answer. Maybe the customer's system should pull it via API. Maybe the concept of "sending an invoice" is a legacy artefact of a paper-based process that software doesn't need to replicate at all. Nobody asked, because the story was a ticket, not a question.&lt;/p&gt;

&lt;p&gt;Treat the story as a discussion item instead — as new information about a domain the team is trying to understand, not a task to be executed — and the entire dynamic changes. The engineer's job becomes domain archaeology: stripping the legacy how from the domain owner's description to find the what underneath. What problem are you actually solving? What would good look like if you had no constraints from the way you currently do it? What would disappear from your working day if this worked perfectly?&lt;/p&gt;

&lt;p&gt;Those questions are uncomfortable. They require the domain owner to separate themselves from their own practice. They require the engineer to be genuinely curious about a domain they don't live in. But that discomfort is where the model gets built — and the model is what the software should reflect.&lt;/p&gt;

&lt;p&gt;This is also why two or three weeks without domain contact is so dangerous. Every day the engineer works from the story-as-ticket, they make decisions based on their current understanding of the domain. Each decision becomes the foundation for the next. By the end of the sprint, the assumptions are load-bearing. Changing them isn't a story revision. It is structural rework. And the DoD, dutifully signed off, certifies a structure built on assumptions nobody tested.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Sprint Is a Learning Unit
&lt;/h2&gt;

&lt;p&gt;The most persistent misunderstanding in Scrum practice is what a sprint is for.&lt;/p&gt;

&lt;p&gt;A sprint is not a delivery unit. It is a learning unit. The question it should answer is not "what did we complete?" but "what do we understand now that we didn't understand before, and does the software reflect that understanding?"&lt;/p&gt;

&lt;p&gt;This distinction changes everything about how the sprint runs, how the review is conducted, and what the next sprint is for.&lt;/p&gt;

&lt;p&gt;If the sprint is a delivery unit, the review is a closing ceremony. Stories are demonstrated. Sign-offs are gathered. The board is cleared. Planning begins for the next batch. The product at the end of the sprint is the output. The backlog is the input for the next cycle.&lt;/p&gt;

&lt;p&gt;If the sprint is a learning unit, the review is an opening conversation. The product at the end of the sprint is not the output — it is the new starting point. The most important question in the room is not "did we build what we planned?" but "given what we've built and what we've learned, where does this point next?"&lt;/p&gt;

&lt;p&gt;This is why the business owner seeing the product for the first time at the demo is a signal that something has gone wrong — not right. By the time the demo happens, they should already know what's there, because they have been part of the conversation as it developed. The demo is a show and tell for the wider organisation — stakeholders, interested parties, people who benefit from visibility into progress. It is valuable for that. It is not the primary feedback mechanism. The primary feedback mechanism is the continuous conversation between engineer and business owner that has been happening all sprint, informally, around working software that is always current enough to think with.&lt;/p&gt;

&lt;p&gt;The backlog, in this model, is not a queue of pre-specified work. It is a set of open questions — hypotheses about what the product needs to become, held loosely and revised continuously as understanding grows. The long-term planning the product owner holds is directional, not prescriptive: functional areas, broad horizons, strategic intent. The specific shape of each sprint emerges from where the product currently stands and what the team has most recently learned.&lt;/p&gt;

&lt;p&gt;That is what Takeuchi and Nonaka called multilearning. It was not a process characteristic. It was what inevitably happened when people with full commitment to the outcome worked in direct contact with the domain. The learning was continuous because the consequence of not learning was immediate and personal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Limiting the Chickens
&lt;/h2&gt;

&lt;p&gt;None of this is an argument against scrum masters, agile coaches, or specialised platform teams. There are excellent people in all of those roles — people who compensate for the structural absence of skin in the game through personal commitment, genuine craft, and deep care about outcomes they will never formally be held accountable for.&lt;/p&gt;

&lt;p&gt;But you cannot build a reliable system on character traits. You can admire them. You cannot depend on them at scale. And you cannot ignore what the accumulation of consequence-free authority does to the system around those individuals — however excellent they are.&lt;/p&gt;

&lt;p&gt;The question to ask about every role on or around a Scrum team is not "is this person good at their job?" It is two questions: does this person feel it when the product fails to serve the business? Does this person feel it when the process slows the team down?&lt;/p&gt;

&lt;p&gt;Two yes answers: keep them close, give them authority, trust their judgment. One yes: useful, but watch the ratio. Two no answers: may be excellent. Cannot be the majority. Should not hold process authority over people who answered yes.&lt;/p&gt;

&lt;p&gt;This is not about eliminating external expertise. It is about understanding what external expertise can and cannot provide. A good consultant scrum master brings experience, pattern recognition, and perspective that an internal team member might lack. What they cannot bring is consequence. And when consequence-free authority accumulates — scrum master, agile coach, platform team, architecture review board, all operating with authority over how the work happens but none bearing the outcome — the team learns quickly, and correctly, that the process does not belong to them. It was handed down from outside. So they perform it rather than own it. And a performed process is a checkbox factory almost by definition.&lt;/p&gt;

&lt;p&gt;Scrum was partly designed to dissolve the independent infrastructure team — the group whose dashboard was their product, whose relationship to the actual product was mediated by tickets and queues. DevOps recognised the same problem and tried to dissolve the boundary between build and run. What neither fully resolved was the deeper pattern: that any specialised group whose success metric is their own domain, rather than the outcome of the product, will optimise for their domain. The pipeline will run. The ceremonies will happen. The dashboard will stay green.&lt;/p&gt;

&lt;p&gt;The answer is not to abolish specialisation. It is to ensure that the people who feel the consequence of the product's success or failure are never outnumbered and never out-authorised by the people who don't.&lt;/p&gt;




&lt;h2&gt;
  
  
  Back to Honda
&lt;/h2&gt;

&lt;p&gt;Honda's engineers did not have a Definition of Done. They had a standard they could not compromise, enforced not by a checklist but by the complete absence of distance between themselves and the consequences of falling short.&lt;/p&gt;

&lt;p&gt;They did not have a product owner translating customer needs into stories. They had customers whose reactions to prototypes shaped the next iteration of the design directly, through the hands and judgment of the people doing the work.&lt;/p&gt;

&lt;p&gt;They did not have a process coach facilitating their ceremonies. They had senior engineers whose authority came from depth of knowledge and shared consequence — people who removed obstacles because the obstacles were in their way too.&lt;/p&gt;

&lt;p&gt;What Takeuchi and Nonaka observed was not a process. It was what process looks like when it is fully owned by the people who cannot afford for it to fail. The ceremonies that mattered emerged from the work. The ones that didn't, didn't happen — because nobody with skin in the game had time for them.&lt;/p&gt;

&lt;p&gt;Scrum, at its best, is an attempt to recreate that condition in software teams. The framework is sound. The ceremonies are scaffolding. The roles are starting points. None of them are the point.&lt;/p&gt;

&lt;p&gt;The point is consequence density. Who in the room cannot afford to be wrong? Who will feel it tomorrow if the model is off? Who has no dashboard to hide behind and no next engagement to retreat to when the product fails?&lt;/p&gt;

&lt;p&gt;Keep those people close. Give them authority. Let the process serve them rather than the other way around. Make the business owner's continuous presence the normal condition rather than the exceptional one. Treat the story as a question, not a ticket. Let the sprint answer it. Let the product as it stands be the permanent starting point for the next conversation.&lt;/p&gt;

&lt;p&gt;And when you feel the urge to add another sign-off, another role, another ceremony — ask first whether you are closing a genuine gap or substituting for a conversation that should just happen.&lt;/p&gt;

&lt;p&gt;Because if the business owner is in the room, you already know the answer. And you don't need a checkbox to confirm it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is part of a series on software engineering craft. The previous piece, "The Gods That Ate the Engineers," examines how the broader software industry mistook its tools for its craft.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>softwaredevelopment</category>
      <category>scrum</category>
      <category>agile</category>
    </item>
    <item>
      <title>The Gods That Ate the Engineers</title>
      <dc:creator>Leon Pennings</dc:creator>
      <pubDate>Wed, 27 May 2026 05:55:38 +0000</pubDate>
      <link>https://dev.to/leonpennings/the-gods-that-ate-the-engineers-210h</link>
      <guid>https://dev.to/leonpennings/the-gods-that-ate-the-engineers-210h</guid>
      <description>&lt;h2&gt;
  
  
  How software development mistook its tools for its craft — and what it is paying for that mistake
&lt;/h2&gt;

&lt;p&gt;There is a conversation that happens in software teams every day. Someone proposes a simpler approach. Someone else says "but we need this to scale." The first person asks what scale is actually required. The second person explains that the architecture team has decided on the standard stack. The first person points out that the application has twenty-five users. The second person suggests talking to the infrastructure architect.&lt;/p&gt;

&lt;p&gt;The conversation ends there. Not because the technical argument was resolved. Because the two engineers were no longer speaking the same language. One was speaking the language of context — what does this problem actually require? The other was speaking the language of compliance — what does the standard say we should do? Those two languages have no shared grammar. The conversation cannot proceed, so it escalates instead.&lt;/p&gt;

&lt;p&gt;This is not a story about stubbornness. It is a story about a profession that has progressively lost the vocabulary of first principles, and replaced it with the vocabulary of tools — and what happens when the people who should be having the hard conversation have never been taught the words.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Measurement Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Software engineering has a property no other engineering discipline shares: its quality is almost entirely invisible.&lt;/p&gt;

&lt;p&gt;A bridge that is over-engineered costs more to build. A building with poor thermal design costs more to heat. Even a book that doesn't serve its readers fails to sell. In each case there is a signal — a cost, a measurement, a market response — that connects engineering decisions to outcomes.&lt;/p&gt;

&lt;p&gt;Software has one test: does it work? If the application runs in production, the engineering passes. If it doesn't, it fails. There is no measurement for whether it could have been built in a fraction of the time with a fraction of the complexity. Nobody built that version. There is no reference to compare against.&lt;/p&gt;

&lt;p&gt;This is not just a gap in measurement. It is the foundational problem of the entire discipline. Because when the only validation is "it works," everything that produces working software becomes equally valid. The team that spent three months on spikes and produced a distributed microservices architecture that nobody fully understands — it works. The team that spent one day with domain experts, modeled the core concepts, and built a coherent system in three weeks — it also works. The outcomes look identical. The costs are incomparable.&lt;/p&gt;

&lt;p&gt;Fred Brooks captured this tragedy in 1986: every system is built only once. There is no second version built with different assumptions, run for five years, and compared on total cost of ownership. The counterfactual does not exist. The cost of bad decisions is permanently invisible.&lt;/p&gt;

&lt;p&gt;What fills the vacuum left by absent measurement? Authority. Convention. And demigods.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Rise of the Demigods
&lt;/h2&gt;

&lt;p&gt;A demigod is not a false god. That is important. A false god has no power. A demigod has real power — but finite power, power over a specific domain, power that has limits it will not advertise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TDD&lt;/strong&gt; is a demigod. It genuinely reduces certain classes of bugs. It creates a feedback loop between intention and implementation. Used with understanding, it is a valuable practice. But TDD defines the questions before it discovers the theory. Write the test, make it pass. The test describes an action — a thing the system should do. It says nothing about the mechanism that should enable that action, the underlying structure that would make the action natural rather than bolted-on. You can TDD your way to a perfectly tested mess. The tests are green. The architecture is incoherent. The demigod delivered what it promised and nothing more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CQRS&lt;/strong&gt; is a demigod. Separating reads from writes treats a real symptom — but that symptom is often produced by a deeper failure. When reads and writes conflict, it is frequently because the domain model isn't carrying its weight: state is inconsistent, rules are scattered, the persistence layer has leaked into everything. CQRS resolves the tension by physically separating it, at significant architectural cost, while the cause goes unexamined. The mess that made CQRS feel necessary is sealed behind the architecture and forgotten.&lt;/p&gt;

&lt;p&gt;The conventional wisdom holds that complex domains — high-scale transactional systems, regulated industries, extreme concurrency requirements — genuinely justify this kind of architecture. The conventional wisdom has it backwards. Those are precisely the domains where a behavior-carrying domain model would deliver the most value, making invariants explicit and enforcing consistency rules at the model level rather than externalizing them into orchestration layers and read/write splits. What looks like sophisticated enterprise architecture is, in many cases, sophisticated coping with a modeling failure that the architecture was never asked to fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Microservices&lt;/strong&gt; are a demigod. &lt;strong&gt;Scrum&lt;/strong&gt; is a demigod. Each of them originated as an observation — someone looked at good engineering practice, noticed a pattern, and named it. The name spread. The observation became a methodology. The methodology became a certification. The certification became a hiring criterion. And somewhere in that journey, the principle the observation was pointing at quietly disappeared.&lt;/p&gt;

&lt;p&gt;What remains is ceremony. Scrum was an insight about feedback loops: build something small, expose it to reality, learn, adjust. Now it is planning poker, velocity points, and a definition of done. The ceremonies survived. The epistemology was discarded. You can run perfect Scrum and never once have a conversation that deepens your understanding of the domain you are building for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spring&lt;/strong&gt; is a demigod — and the most instructive one, because it did not merely obscure first principles. It industrialized their replacement.&lt;/p&gt;

&lt;p&gt;Spring's recipe is seductive in its clarity: a controller receives the request, a service orchestrates the logic, a repository handles the persistence. Learn the recipe and you can implement almost any user story. The pattern is consistent, communicable, and scales across teams. It is also procedural programming wearing object-shaped clothing. The service class becomes the address for all behavior, because the recipe has no concept of behavior belonging to the domain objects themselves. Every new requirement gets the same answer: add a method to the service. The mechanism — the structure of the business domain, the responsibilities of its concepts, the rules that govern its behavior — is never considered, because the recipe answered the structural question before you asked it.&lt;/p&gt;

&lt;p&gt;This is not a side effect of Spring. It is what Spring teaches. A library gives you capabilities and leaves the thinking to you. Spring gives you the thinking pre-done. Engineers who learned Spring as their foundation did not learn to reason about structure — they learned to apply a structure that was handed to them. When the recipe always fits, you never develop the judgment to know when it doesn't. The capacity atrophies quietly, and working software confirms at every step that nothing is wrong.&lt;/p&gt;

&lt;p&gt;Spring did not create the anemic domain model. But it mass-produced it, certified it, and made it the industry default. It turned a modeling failure into a career path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI&lt;/strong&gt; is a demigod — the latest, the most powerful, and the most dangerous one the profession has yet encountered.&lt;/p&gt;

&lt;p&gt;AI is genuinely transformative at the implementation level. It can generate code, implement features, navigate unfamiliar frameworks, and eliminate enormous amounts of repetitive work. In a well-understood domain with an explicit model, it is an extraordinary accelerator — handling the mechanical expression of things the engineer already understands. That is real and significant power.&lt;/p&gt;

&lt;p&gt;But AI has the same hard limit every demigod has. It cannot ask what the mechanism should be before implementing the action. It cannot determine whether a concept belongs in the domain model or whether it is accidental complexity in disguise. It cannot notice that the service class has become a procedural script, or that the architecture has answered the structural questions before anyone understood the structure. Give AI a well-modeled domain and it accelerates good engineering. Give it a recipe and a backlog and it produces Spring-shaped procedural code at a speed no human team could match — complete with tests, documentation, and a green pipeline, none of which will tell you that the map was never drawn.&lt;/p&gt;

&lt;p&gt;The previous demigods papered over the absence of first principles. AI industrializes that papering at a velocity that makes the underlying absence nearly impossible to see and nearly impossible to recover from. The mess accumulates faster than any previous generation of engineers could have produced it. Every demigod arrived as a silver bullet. AI is the latest — and the profession is following the pattern with the same fidelity it always has.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Tools Become Identity
&lt;/h2&gt;

&lt;p&gt;Here is where the measurement problem and the demigod problem combine into something more serious — and where the economic machinery that drives the industry becomes visible.&lt;/p&gt;

&lt;p&gt;Software development scaled faster than the supply of engineers who understood it deeply. The response was industrialization. If you are running a software factory, you need interchangeable parts. Interchangeable engineers require standardized tools. You cannot factory-manage engineering judgment — it is invisible, contextual, slow to assess, and impossible to replicate at scale. But you can factory-manage Spring Boot certification. You can standardize on Kubernetes. You can mandate the architecture diagram before the domain conversation happens, because the architecture diagram fits into a project timeline and engineering judgment does not.&lt;/p&gt;

&lt;p&gt;The factory model did not choose tools over judgment because it was ignorant of the difference. It chose tools because tools are manageable and judgment is not. That choice, made millions of times in hiring decisions and project kickoffs and architecture reviews, compounded into an industry.&lt;/p&gt;

&lt;p&gt;The economic incentives completed the picture. An engineer cannot put "sound engineering judgment" on a CV. They can put Kubernetes, Kafka, Spring Boot, and AWS. The market rewards tool-hoarding because tool-hoarding is legible and judgment is not. So engineers rationally invest in tools. They accumulate certifications. They learn the next framework. The career incentive and the factory requirement point in the same direction, and the profession follows.&lt;/p&gt;

&lt;p&gt;The consequence is a generation of practitioners who were never taught the underlying principles — not because they are poor engineers by disposition, but because the path through the profession did not require those principles. Framework knowledge was sufficient. It got them hired. It gets features shipped. It passes the only test anyone applies.&lt;/p&gt;

&lt;p&gt;This is where the Dunning-Kruger effect enters — and it enters structurally, not individually. When "it works" is the only feedback signal, the gap between tool expertise and engineering judgment produces no visible failures. The feedback loop that would expose the gap never fires. An engineer who has only ever navigated by Spring's recipe has no evidence that another kind of navigation exists, because both arrive at working software.&lt;/p&gt;

&lt;p&gt;What happens when that engineer is challenged on a technical decision? They cannot retreat to first principles, because those principles were never their foundation. They can only defend the tool. And defending the tool looks like defending engineering — because in the world they have always inhabited, they are the same thing.&lt;/p&gt;

&lt;p&gt;This is why the conversation about the build agent ends with "talk to the infrastructure architect." Not stubbornness. Not bad faith. The argument has moved to terrain where their map does not reach, and the only available response is to invoke authority rather than reasoning. The map was never drawn because nobody required it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cost of Working Software
&lt;/h2&gt;

&lt;p&gt;At this point a reasonable person might object: so what? It works, doesn't it? The software ships. The business runs. Teams are productive. Perhaps the architecture is heavier than it needs to be, but that is a philosophical concern, not a practical one.&lt;/p&gt;

&lt;p&gt;It is not a philosophical concern. It has a price. And that price is paid in headcount, infrastructure spend, and organizational mass — every month, permanently, at a scale most organizations have never stopped to calculate because they have nothing to compare it against.&lt;/p&gt;

&lt;p&gt;Start with the team. A Scrum-based delivery organization does not just have engineers. It has product owners to translate business needs into stories, scrum masters to run the ceremonies, agile coaches to optimize the ceremonies, and program managers to coordinate across the teams that have multiplied because the architecture decomposed the system into services each requiring ownership. None of these roles existed before the ceremony required them. They are not a consequence of software complexity. They are a consequence of the process layer that was wrapped around it.&lt;/p&gt;

&lt;p&gt;The infrastructure follows the same logic. A well-modeled application, sized honestly to its problem, might run on a handful of servers with a deployment process a single engineer can understand. The standard stack requires container orchestration, service meshes, distributed tracing, centralized log aggregation, secrets management, cloud cost governance, and a security perimeter that scales with the number of services rather than the complexity of the domain. Someone has to build and own that infrastructure — which means an infrastructure team. Someone has to own the pipeline tooling — which means a platform team. Someone has to operate the observability stack that exists entirely because the system is too opaque to reason about directly — which means an observability practice, which means tooling budgets, which means vendor contracts.&lt;/p&gt;

&lt;p&gt;Count it all. The ceremony layer, the infrastructure department, the platform team, the observability tooling, the architecture review board that exists because the architecture requires governing. Compare it to a team organized around an honest domain model, sized to the actual problem, with infrastructure that serves the domain rather than managing the accidental complexity the domain was never asked to absorb.&lt;/p&gt;

&lt;p&gt;The difference in team size is not marginal. Doubling is optimistic. Tripling is closer. When infrastructure and tooling costs are included, the multiplier on total cost of ownership reaches further than most organizations want to calculate — because the calculation would require admitting that the standard stack is not an engineering choice. It is an organizational commitment, billed indefinitely, justified by working software that could have been built and maintained at a fraction of the cost by a team that understood what it was building.&lt;/p&gt;

&lt;p&gt;The most expensive software is the software everyone agrees is fine.&lt;/p&gt;




&lt;h2&gt;
  
  
  What First Principles Actually Means
&lt;/h2&gt;

&lt;p&gt;First principles in software engineering are not a methodology. They are not a framework. They cannot be certified.&lt;/p&gt;

&lt;p&gt;In any engineering discipline, first principles means reasoning from what is actually true about the problem — from the undeniable constraints of physics, economics, or logic — before selecting any tool or approach. In bridge building, you start with loads, materials, and forces. In software, the undeniable truth is the business domain itself: what it does, what it needs, what rules govern it, what concepts exist within it. Everything else is a choice. The domain is not a choice. It is the ground the system must stand on.&lt;/p&gt;

&lt;p&gt;First principles therefore begins with a single question: what mechanism does this business need, and what is the structure of that mechanism?&lt;/p&gt;

&lt;p&gt;Not: what actions need to happen. Not: what user stories need to be implemented. Not: what does the recipe provide.&lt;/p&gt;

&lt;p&gt;The distinction between actions and mechanisms is the one the entire profession routinely misses — and it is the one that determines everything that follows.&lt;/p&gt;

&lt;p&gt;An action is something the system does. Place an order. Send an invoice. Notify a customer. Actions are visible, speakable, easy to write as user stories. They are also infinite. There is always another action. A system built around implementing actions never reaches coherence — it reaches a different kind of completeness, the kind where every story is closed and nobody can tell you where any particular rule lives.&lt;/p&gt;

&lt;p&gt;A mechanism is the structure that makes actions possible. The domain concepts, their responsibilities, their relationships, the rules they enforce. Mechanisms are finite. A business domain has a bounded set of real concepts — not infinite. Once you understand them, new actions find their natural place. The mechanism does not need to change because a new action arrived; the action was always expressible in terms of the mechanism. You just had not asked for it yet.&lt;/p&gt;

&lt;p&gt;This is why a day spent with domain experts outperforms four sprints of discovery spikes. The spikes are action-oriented. They produce implementations of specific scenarios, each one leaving a deposit of logic somewhere convenient, none of them building toward a coherent structure. The domain conversation is mechanism-oriented. It produces understanding of what the system actually is — and from that understanding, implementations become fast, because they are no longer navigating blind.&lt;/p&gt;

&lt;p&gt;The domain expert knows the story. The engineer's job is to understand the mechanisms that story requires — and then model those mechanisms honestly, directly in code, without a documentation layer between the understanding and the implementation. A whiteboard sketch is a thinking tool. The code is the model. There is no pile of upfront design, no architecture document that creates its own maintenance burden and its own resistance to change. Formal documentation does not just resist change mechanically — it raises the social cost of being right. The person who says the abstraction is wrong is not raising a technical question. They are implicitly criticising the judgment of everyone who approved the document. So people stop saying it. Understanding goes directly into structure, continuously, as the understanding grows.&lt;/p&gt;

&lt;p&gt;This is not big upfront design. It is the opposite. Big upfront design tries to answer everything before building anything. First principles thinking says: understand what is true now, encode it honestly, and stay honest as truth evolves. A payment system models credit cards — until digital wallets arrive, and the domain reveals the real concept was always a payment method. The model grows because the understanding grew. Not because a story was implemented. Because something was learned.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Speed That Nobody Measures
&lt;/h2&gt;

&lt;p&gt;The most persistent myth about principles-first development is that it is slow.&lt;/p&gt;

&lt;p&gt;It is not slow. It is the fastest path available — and it gets faster as it goes, while the alternative gets slower.&lt;/p&gt;

&lt;p&gt;Tool-driven, action-focused development feels fast because it is always moving. Tickets close. PRs merge. Velocity is high. But the team is navigating by taking the next available turn rather than reading the terrain. Enormous distance is covered traveling a short path. Each new feature lands in a codebase without a map, and finding where it belongs takes longer each time, because the codebase is larger and less coherent than it was before.&lt;/p&gt;

&lt;p&gt;Principles-first development feels slower at the start because the team is reading the map. But the map converts future distance into present understanding. Features find their place. The model tells you where things belong. The implementation follows from the understanding — and because the model is clear, the implementation is the smaller part of the work, not the larger.&lt;/p&gt;

&lt;p&gt;The asymmetry compounds over time. Principles-first gets faster. Tool-driven gets slower. They do not just start at different speeds — they move in opposite directions. And because both produce working software, the team on the slower path has no signal that another trajectory exists. The velocity metric measures motion, not progress. You can cover enormous distance going the wrong way and call it delivery.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Conversation We Can No Longer Have
&lt;/h2&gt;

&lt;p&gt;Something has been lost that is harder to recover than a methodology or a framework.&lt;/p&gt;

&lt;p&gt;When two engineers disagree about a technical decision, resolution requires a shared language: first principles. Does this decision reflect the actual complexity of the domain? Is the added mechanism justified by what the domain requires? Is this accidental complexity or essential complexity? Those questions have answers that are reasoned, not asserted. But they require both participants to have internalized the same foundation — to reason from what is true about the problem, rather than advocate from what their tools provide.&lt;/p&gt;

&lt;p&gt;When tool expertise replaces engineering judgment, that conversation becomes structurally impossible. Not because people argue in bad faith, but because they are operating from entirely different premises. One person is asking what the domain requires. The other is asserting what the standard stack provides. These are not positions that can be reconciled by better argument. They are not even positions in the same debate.&lt;/p&gt;

&lt;p&gt;The engineer with first principles asks: what scale do we actually need? The engineer with tools answers: we use the scalable architecture. The first engineer points to the user count. The second engineer escalates to the infrastructure architect. This is not a failure of communication. It is a failure of shared foundation — and the shared foundation was never built, because the profession stopped requiring it.&lt;/p&gt;

&lt;p&gt;The solution is not a new methodology. It is not another demigod. The last thing the industry needs is a certification in first principles thinking. It is the recovery of something quietly discarded as the profession industrialized — the understanding that engineering judgment precedes tool selection, that mechanisms precede actions, that one focused conversation about what the domain actually is will outperform any number of sprints implementing what the domain appears to do.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Only Test That Matters
&lt;/h2&gt;

&lt;p&gt;Software engineering currently applies one test: does it work?&lt;/p&gt;

&lt;p&gt;That test is necessary but nowhere near sufficient. A system can work and be incomprehensible. A system can work and cost ten times what it should have. A system can work and reflect no coherent understanding of the domain it serves. The pipeline is green. The retrospective is positive. The modeling failure is invisible, as it always was.&lt;/p&gt;

&lt;p&gt;The organization built to sustain the demigod stack — the scrum masters and platform teams and observability engineers and architecture review boards — has a structural interest in the stack continuing to be necessary. The demigods do not just persist because engineers worship them. They persist because the organizations that grew up around them cannot afford to question them.&lt;/p&gt;

&lt;p&gt;That is where the profession is. Not failing. Working. Expensively, slowly, with tripled teams and bloated infrastructure and a generation of engineers who were handed a recipe instead of a craft.&lt;/p&gt;

&lt;p&gt;Until someone asks: but what scale do we actually need?&lt;/p&gt;

&lt;p&gt;And the room goes quiet.&lt;/p&gt;

&lt;p&gt;And someone says: talk to the infrastructure architect.&lt;/p&gt;

&lt;p&gt;And nothing changes — until engineers are once again taught that the map comes before the journey, and that knowing how to apply a recipe is not the same as knowing how to think.&lt;/p&gt;

</description>
      <category>java</category>
      <category>architecture</category>
      <category>softwaredevelopment</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Properties of Enterprise Software That Lasts</title>
      <dc:creator>Leon Pennings</dc:creator>
      <pubDate>Thu, 21 May 2026 08:43:16 +0000</pubDate>
      <link>https://dev.to/leonpennings/the-properties-of-enterprise-software-that-lasts-4j50</link>
      <guid>https://dev.to/leonpennings/the-properties-of-enterprise-software-that-lasts-4j50</guid>
      <description>&lt;p&gt;&lt;em&gt;"Perfection is achieved not when there is nothing more to add, but when there is nothing more to remove."&lt;/em&gt; — Antoine de Saint-Exupéry&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Enterprise software is different from other software. Not in the technologies used to build it, not in the frameworks, not in the methodologies. It is different in its purpose: it must work correctly today, remain correct over time, survive the people who built it, and adapt to a business domain that will change in ways nobody can fully predict. Most software is built to solve today's problem. Enterprise software must be built to outlast today's understanding.&lt;/p&gt;

&lt;p&gt;That is a fundamentally different design goal. And it demands a fundamentally different way of thinking about software — about what matters, what doesn't, and what the job of a developer actually is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The code is downstream of the thinking.&lt;/strong&gt; The properties which determine whether enterprise software survives — or quietly becomes the system nobody dares touch — are not primarily technical. They are properties of understanding. And the thinking starts long before the first line is written.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Six Properties
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Longevity
&lt;/h3&gt;

&lt;p&gt;The core of enterprise software should, in retrospect, survive ten to fifteen years. Not the UI framework. Not the ORM. Not the messaging library. The &lt;em&gt;core&lt;/em&gt; — the domain logic, the structural decisions, the way the system understands and represents the business.&lt;/p&gt;

&lt;p&gt;This sounds obvious until you consider how rarely it is treated as a design constraint. Most development decisions are made under short-term pressure: the sprint deadline, the current team's preferences, the framework that is fashionable today. None of those inputs have any relationship to what the system will need to be in year eight.&lt;/p&gt;

&lt;p&gt;Longevity is not achieved by predicting the future. It is achieved by not over-committing to the present. Every unnecessary dependency, every piece of logic tied to a specific framework's idiom, every abstraction built around today's tooling rather than today's domain — these are bets that the present will continue. In enterprise software, the present never continues long enough.&lt;/p&gt;

&lt;p&gt;Longevity is the north star. The properties that follow are the means to achieve it.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Upgradeability
&lt;/h3&gt;

&lt;p&gt;Upgradeability is not about keeping dependencies current. Keeping dependencies current is maintenance. Upgradeability is structural: it is the capacity of the system to accept functional change without requiring a rewrite of its core.&lt;/p&gt;

&lt;p&gt;This distinction matters enormously. A system can have perfectly up-to-date dependencies and be completely unupgradeable — because its structure was built around the features known at the time, implemented in a way that assumes those features are the final shape of the domain. When the business changes, and it will, there is nowhere to go.&lt;/p&gt;

&lt;p&gt;Building for upgradeability means building with the understanding that what you know today is not everything. It does not mean building features you don't need — that is the opposite of the principle. It means implementing what you know today in a way that does not foreclose tomorrow. The structure should be open to extension, refactoring, and replacement at the right level of granularity.&lt;/p&gt;

&lt;p&gt;This is also where the conventional wisdom about test coverage becomes a liability. Class-level unit tests — one test class per production class, testing the internal mechanics of each — are a contract on the current implementation. They make refactoring expensive by breaking whenever the internals change, even when the behavior is preserved. Over time, they become the reason the system cannot be restructured: the test suite has calcified the implementation.&lt;/p&gt;

&lt;p&gt;Behavioral tests — tests that assert what a piece of functionality does, not how a particular class does it — are a contract on the domain. They survive refactoring because refactoring does not change behavior, only implementation. Upgradeability requires the right level of test coupling. Tests should be coupled to what the system does, not to how it currently does it.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Maintainability
&lt;/h3&gt;

&lt;p&gt;Maintainability in long-lived software is primarily a question of dependency discipline. Every external dependency is a commitment: to a version, to an API contract, to a community that may or may not continue to support it. Over fifteen years, many of those commitments will become liabilities.&lt;/p&gt;

&lt;p&gt;The critical discipline is asking, for every dependency: what does this actually buy us? Not in theory — in practice, in this specific system, for this specific use case. The question is not whether a dependency is good in the abstract — a battle-tested cryptography library, a well-maintained time handling library, a parser for a complex format — these earn their place because the alternative is genuinely worse. The question is whether &lt;em&gt;this&lt;/em&gt; dependency serves &lt;em&gt;this&lt;/em&gt; production system's domain needs, or whether it serves the tooling, the framework preference, or the developer's convenience.&lt;/p&gt;

&lt;p&gt;The dependency that should be rejected without hesitation is the one whose primary justification is testability of the production code. Testability is a testing concern, not a production concern. Production code should not be structured, abstracted, or made more complex to accommodate the needs of the test suite.&lt;/p&gt;

&lt;p&gt;This manifests in two particularly damaging patterns. The first is mocking-driven architecture: interfaces created not because the domain has multiple implementations of a concept, but because the test framework needs a seam to inject a mock. An interface with one real implementation, existing purely to enable a unit test, adds a layer of indirection with no domain justification. Every future reader follows the code, hits the interface, and must go find the implementation. The test was marginally easier to write. Every reader pays for that convenience forever.&lt;/p&gt;

&lt;p&gt;The second is Aspect-Oriented Programming applied to cross-cutting concerns. The promise was clean separation — keep business logic free of logging, transactions, security, caching. In practice, the result is code where you cannot tell what is executing by reading it. The aspects are invisible in the source. Behavior is woven in at runtime by configuration that must be hunted for separately. You need a debugger to understand what your own code does. That is not decoupling. It is hidden coupling, which is strictly worse than visible coupling because at least visible coupling can be read.&lt;/p&gt;

&lt;p&gt;Both patterns share the same failure: a tooling concern reshaped the production code in ways that made it harder to understand. The test suite or the framework became easier to work with. The system became harder to reason about. That is the wrong trade, and it compounds over fifteen years in ways that eventually make the system unreformable.&lt;/p&gt;

&lt;p&gt;The simpler path is to make the production code so clear in its intent that the need for complex testing infrastructure is reduced rather than accommodated. Nobody tests &lt;code&gt;string.trim()&lt;/code&gt; — not because someone decided it was below the testing threshold, but because its intent and behavior are completely transparent. The ambition for domain logic should be the same. &lt;code&gt;order.send()&lt;/code&gt; can be just as obvious if the implementation reads like a statement of business intent rather than a sequence of technical operations.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Extensibility
&lt;/h3&gt;

&lt;p&gt;Extensibility requires locatability. Before you can extend a piece of functionality, you must be able to find it — and find it with confidence that you have found all of it, not just the most obvious part.&lt;/p&gt;

&lt;p&gt;This is where fat services fail. When business logic accumulates in large service classes organised around user stories or features, the domain structure disappears. Logic that belongs together by domain reason is separated. Logic that is separate by domain reason collides in the same class. Over time, the service becomes an archaeological record of every feature request, in chronological order, and understanding what it does requires reading its entire history.&lt;/p&gt;

&lt;p&gt;Extensibility is only achievable when the code is structured around the domain — around what the business actually is, not around how it was requested. When that structure exists, adding a new capability means finding the right place in a coherent map. When it does not exist, extending the system means navigating a maze and hoping you found everything relevant.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Readability
&lt;/h3&gt;

&lt;p&gt;Readability is not a soft property. It is not aesthetic. It has direct economic consequences over a fifteen-year lifespan that compound in ways that eventually make a system unreformable.&lt;/p&gt;

&lt;p&gt;The measure of readability in enterprise software is not whether an experienced developer finds the code elegant. It is whether the intent and structure are followable to a non-engineer — a domain expert, a compliance officer, a business analyst — who can read the code and recognise their domain in it. This does not mean every line reads as plain prose. Some domains have irreducible technical density: complex financial calculations, regulatory rule engines, actuarial models. The bar is not that the implementation is self-explanatory to someone without domain expertise. The bar is that the &lt;em&gt;structure&lt;/em&gt; expresses the domain, that the &lt;em&gt;intent&lt;/em&gt; is visible, and that the domain expert can follow the logic well enough to identify where their understanding is or is not correctly represented.&lt;/p&gt;

&lt;p&gt;If the code reads like hocus pocus at the structural level to the person who understands the business, the code has failed at its most important communication task.&lt;/p&gt;

&lt;p&gt;This standard has consequences for every micro-decision in implementation. It argues against stream operations where a for-loop is clearer to a broader audience — not because streams are wrong, but because in domains where large in-memory sets are never permitted by design, the performance justification evaporates and only the readability cost remains. It argues against boilerplate reduction that sacrifices expressiveness for terseness. It argues against every clever idiom that shortens the code for its author while lengthening the cognitive load for its future readers.&lt;/p&gt;

&lt;p&gt;"Boilerplate" is only boilerplate if it has no business purpose. Code that is verbose because it is expressing a business process is not boilerplate — it is documentation, in the only place documentation is always current. The argument to reduce it is always an argument to optimise for the writer. In enterprise software, the reader is nearly always more important. The code will be read an order of magnitude more times than it is written, by people who were not present when it was created.&lt;/p&gt;

&lt;p&gt;On large data sets specifically: the correct architectural response is not to optimise how they are processed in memory — it is to enforce a boundary that prevents unbounded datasets from reaching the application layer at all. Chunk the data before it is loaded. This is an architectural constraint, not a performance trick. By making large in-memory sets structurally impossible, the design eliminates the entire class of optimisation pressure they create. The complexity of cursor management and pagination lives at the data access boundary, where it belongs, not scattered as stream operations through business logic. The upstream constraint produces downstream simplicity.&lt;/p&gt;

&lt;p&gt;Readability is the condition that makes the other properties achievable. Code that reads like the domain can be upgraded because the domain is visible in it. Code that expresses intent clearly can be maintained because its purpose is self-evident. Code that maps the domain accurately can be extended because the map can be followed. It is not one property among five — it is the keystone.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. Organisation
&lt;/h3&gt;

&lt;p&gt;Organisation is qualitatively different from the first five properties. Those are visible in the codebase — you can read them, measure them, argue about them in a code review. Organisation is visible in what the codebase was &lt;em&gt;allowed to become&lt;/em&gt;. It is the soil in which the other properties grow or fail to grow. Making it an explicit pillar says: this cannot be managed by ignoring it.&lt;/p&gt;

&lt;p&gt;The question every development team eventually confronts is whether the organisation is supportive or restrictive. The honest answer is that it is almost always intended to be supportive and frequently experienced as restrictive — and the gap between those two is where a significant amount of enterprise software complexity originates.&lt;/p&gt;

&lt;p&gt;The most common form this takes is architectural mandate without domain justification. Platform teams, rightly responsible for consistency and infrastructure standards, apply patterns designed for large distributed systems universally — including to applications that are, by domain definition, a single coherent thing. Microservices architectures get mandated for systems with no independent scaling requirements, no team boundary that would justify a service boundary, no domain reason for a network boundary to exist. The result is artificial complexity: deployment pipelines for services with no independent reason to exist, network calls where function calls would suffice, operational overhead that consumes development capacity without adding production value.&lt;/p&gt;

&lt;p&gt;The architecture was not wrong for all systems. It was wrong for this system, for this domain, at this scale. But the mandate did not ask about the domain. It asked about organisational standards. And the production system pays the difference on every deployment, every change, every new hire who must learn the infrastructure before they can touch the domain.&lt;/p&gt;

&lt;p&gt;This is organisational complexity billed to the production system. It feels like support. From the production system's perspective it is an undiscussed tax with no domain justification.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Toyota Parallel
&lt;/h4&gt;

&lt;p&gt;Toyota solved this problem in manufacturing and the solution translates directly to software development. The Toyota Way rests on two pillars: continuous improvement, and respect for people. Both are violated by the organisational patterns that produce restrictive environments.&lt;/p&gt;

&lt;p&gt;Respect for people, in the Toyota sense, is not about workplace culture. It is an epistemological principle: the people closest to the work hold the most valuable knowledge about the work. On the production floor, the assembly worker who notices something wrong knows something the engineer in the office does not. Toyota's andon cord exists to make that knowledge immediately actionable — any worker can stop the line when they identify a defect, because the cost of a defect that travels further down the line is exponentially higher than the cost of stopping to fix it now.&lt;/p&gt;

&lt;p&gt;In software development the people closest to the work are the developers and the domain experts. The domain expert who says "this doesn't reflect how we actually work" is pulling the andon cord. The developer who identifies a structural problem in the architecture is pulling the andon cord. Organisations that route those signals through layers of translation — product owners, project managers, UX designers, platform architects — are not being more rigorous. They are covering the cord in bureaucratic insulation and walking past it.&lt;/p&gt;

&lt;p&gt;The second Toyota concept worth applying directly is &lt;em&gt;genchi genbutsu&lt;/em&gt; — go and see for yourself. Do not manage from reports. Do not accept translated summaries. Go to where the work happens and observe it directly. For software this means the developer sitting with the domain expert, watching them work, seeing where the system creates friction, understanding the domain from its source rather than from a requirements document that passed through three people before it arrived. Every layer of translation between the domain expert and the developer is a layer where meaning is lost and assumption is substituted.&lt;/p&gt;

&lt;p&gt;The third is &lt;em&gt;jidoka&lt;/em&gt; — quality built in, not inspected in after the fact. You cannot UX-design your way to a correct domain model. You cannot test your way to a correct domain model. The correctness must be present from the beginning, in the understanding that shaped the implementation. When domain feedback arrives late — filtered through contact persons who are not the domain authorities, interpreted as a UX problem rather than a domain problem — the system has already been built around an incomplete model. Correcting it at that point is expensive. The organisational structure that produced the late feedback is the root cause, not the feedback itself.&lt;/p&gt;

&lt;h4&gt;
  
  
  Domain Feedback Is Always a Learning Opportunity
&lt;/h4&gt;

&lt;p&gt;When domain experts say a system is too complex or doesn't make sense to them, the instinct in process-first organisations is to call a UX designer. This is solving the wrong problem at the wrong layer. UX is interface orientation — it makes existing concepts easier to navigate. It cannot fix a missing concept. If the domain model is incomplete, no amount of interface polish makes it clearer. You cannot design your way around a hole in the domain.&lt;/p&gt;

&lt;p&gt;"Too complex" from a domain expert almost always means one of two things: a concept that exists in their mental model is absent from the system, or the system is telling a story the domain expert doesn't recognise as their own. Both are domain problems. The correct response is a domain conversation, not a design review.&lt;/p&gt;

&lt;p&gt;This reframes what domain feedback actually is. It is not obstruction. It is not a sign that the users don't understand the system. It is the most valuable signal available — an authoritative source reporting that the model is incomplete. Organisations that treat it as a learning opportunity produce better software. Organisations that treat it as a user adoption problem produce expensive workarounds for incorrect models.&lt;/p&gt;

&lt;h4&gt;
  
  
  Discovery-Driven Implementation
&lt;/h4&gt;

&lt;p&gt;The organisational conditions described above — domain experts who can reach the development team, feedback treated as learning, developers trusted to inquire beyond the story — enable something that process-constrained environments make nearly impossible: discovery-driven implementation.&lt;/p&gt;

&lt;p&gt;Most software development is story-driven. The solution space is bounded by what was requested. The developer's job is to implement the described behaviour correctly and completely. This produces correct implementations of incomplete specifications, reliably and at scale.&lt;/p&gt;

&lt;p&gt;Discovery-driven implementation starts from the same user story but treats it as a symptom description rather than a solution specification. The developer who asks enough questions about the domain — who wants to understand not just what was asked but why, what problem it actually solves, what the current process costs, where it fails — occasionally discovers that the problem as described is not the real problem. The real problem is upstream. And the solution to the real problem makes the described problem structurally impossible rather than better managed.&lt;/p&gt;

&lt;p&gt;This kind of insight cannot be mandated. It cannot be specified in advance. It cannot be written as a test before it exists. It emerges from genuine engagement with the domain, from the developer who treats the user story as a starting point rather than a work order, from the organisation that protects the space for that inquiry rather than constraining every hour to story execution.&lt;/p&gt;

&lt;p&gt;The deepest return on domain understanding is not better implementation of what was asked. It is the occasional recognition that the problem as described is a symptom — and that the real solution makes the symptom structurally impossible. That insight cannot be mandated, cannot be specified, cannot be tested before it exists. It emerges from genuine engagement with the domain, and it is available only to the developer who treated the user story as a starting point rather than a work order. Organisations that protect that space — that trust developers to inquire, to discover, to propose solutions nobody asked for because nobody knew to ask — produce software that solves real problems. Organisations that constrain that space to story execution produce software that manages symptoms, expensively, forever.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Foundation Beneath the Properties
&lt;/h2&gt;

&lt;p&gt;Every property described above is downstream of something that is not a technical practice at all. It is understanding.&lt;/p&gt;

&lt;p&gt;You cannot write readable code about something you do not understand. You cannot structure something well that you have not thought through. You cannot know what to leave out — which is often more important than knowing what to put in — unless you understand the domain well enough to recognise what is essential and what is incidental.&lt;/p&gt;

&lt;h3&gt;
  
  
  The User Story Is Not a Work Order
&lt;/h3&gt;

&lt;p&gt;A user story is a starting point for a conversation, not a specification for implementation. The moment a developer treats it as a work order — something to be implemented against acceptance criteria, tested to green, and closed — they have accepted someone else's translation of the domain as complete and correct. That translation is almost never complete, and sometimes critically incorrect.&lt;/p&gt;

&lt;p&gt;The developer's job before the first line of code is to understand the business goal behind the story. Not the described behaviour — the goal. This requires asking questions. Not to clarify ambiguous requirements, but to understand the domain itself. What is this actually trying to achieve? What are the edge cases the domain expert considers obvious? What should this system never do, and why?&lt;/p&gt;

&lt;p&gt;Consider a user story about calculating UBO — Ultimate Beneficial Ownership. A developer implementing against the story might write: find all natural persons with ownership percentage above the threshold. That is what the acceptance criteria describe. The tests pass. The implementation is wrong.&lt;/p&gt;

&lt;p&gt;A correct understanding of UBO reveals that it is not about direct ownership percentage in isolation. It is about effective control — who ultimately determines the decisions of the entity, regardless of how the ownership structure is arranged. The question is not just who is the UBO. It is who &lt;em&gt;else&lt;/em&gt; is the UBO. And it is who &lt;em&gt;also&lt;/em&gt; has control. If there is no "also" — there is just one.&lt;/p&gt;

&lt;p&gt;That small shift in framing immediately surfaces a class of scenarios that the acceptance-criteria reading misses entirely. Consider natural person 1 who holds 4% in company A and 4% in company B. Company A holds 96% in company B. Company B holds 96% in company A. By direct ownership percentage, natural person 1 appears below the UBO threshold. By effective control, natural person 1 is 100% the UBO of both companies — because the circular cross-ownership means neither company has any independent shareholder beyond this person.&lt;/p&gt;

&lt;p&gt;No test-first methodology surfaces this. No refactoring produces it. Domain understanding produces it, in the conversation before a line of code is written, because a developer who understands what UBO law is actually designed to do recognises this scenario not as an edge case but as a textbook example of what the law was written to catch.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the Implementation Should Not Be
&lt;/h3&gt;

&lt;p&gt;Domain understanding does not only tell you what to build. It tells you what not to build — and that is often more valuable.&lt;/p&gt;

&lt;p&gt;When you understand that UBO is about effective control through any structure, you immediately know the implementation should not be a threshold check on direct ownership percentages. That single "should not" eliminates the naive implementation before it is written. It eliminates an entire class of wrong solutions without a single line of code.&lt;/p&gt;

&lt;p&gt;This is the discipline of subtraction. Every constraint that comes from genuine domain understanding is a constraint that prevents future complexity. What is not there cannot introduce a bug. What is not there requires no maintenance. What is not there cannot become the thing nobody dares touch because nobody understands why it exists.&lt;/p&gt;

&lt;p&gt;The simplest correct solution is also the most durable one. Not because simplicity is aesthetically preferable, but because complexity compounds. Every unnecessary abstraction, every dependency added for theoretical future benefit, every pattern introduced for a problem the system does not have — each one is a tax on every future change, every new hire, every upgrade cycle. Over fifteen years those taxes become the reason a system becomes unreformable.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Right Level of Test Coverage
&lt;/h3&gt;

&lt;p&gt;Honest test coverage in enterprise software is not a percentage target. It is a risk assessment.&lt;/p&gt;

&lt;p&gt;The question is never "what percentage of lines are covered?" It is: "where are the places this system could be silently wrong, and how quickly would we know?" Tests earn their place where the real-world feedback loop is too slow, too infrequent, or too opaque to catch failures naturally.&lt;/p&gt;

&lt;p&gt;A login page that breaks gets reported within minutes — high-frequency paths like these are well covered by integration, smoke, and end-to-end tests that run as part of any competent CI pipeline. Deep unit testing of those flows is redundant effort. A UBO calculation might run once a day for a small compliance team. It could be wrong for weeks before anyone notices. The domain is complex enough that failures are non-obvious. That is precisely where a behavioral test earns its place: not as a development guiderail, but as a specification of correctness for something that does not announce when it is wrong.&lt;/p&gt;

&lt;p&gt;In practice, this produces test coverage in the range of 30 to 50 percent — not because the rest of the code is untested, but because the rest of the code is covered by higher-level tests and validated continuously by the people using it. The 30 to 50 percent that is explicitly tested at the unit or behavioral level is the core domain logic: the calculations, the rule evaluations, the business-critical paths where silent failure is a real and consequential risk.&lt;/p&gt;

&lt;p&gt;This is a more defensible position than 90 percent coverage that includes getters, setters, login flows, and string formatting. Coverage as a metric measures lines executed, not correctness guaranteed. Behavioral tests on the domain core, combined with integration tests on the main flows and a system simple enough that its failures are visible, produces better assurance than a heavily instrumented suite that tests implementation details nobody will care about in year seven.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Training Wheels Problem
&lt;/h3&gt;

&lt;p&gt;There is a pattern in software development where tests function not as a quality mechanism but as a substitute for understanding. If the developer does not fully understand what they are building, green tests provide a guiderail: as long as the tests pass, the implementation is probably acceptable.&lt;/p&gt;

&lt;p&gt;Training wheels do not teach balance. They teach riding without balance — a different skill entirely. A developer conditioned by green tests as their primary signal learns to satisfy the tests. A developer who understands the domain learns what the business actually needs. Those are not the same education, and in complex domains they produce starkly different results.&lt;/p&gt;

&lt;p&gt;The test suite becomes a confidence mechanism decoupled from correctness. The tests reflect the developer's mental model of the domain. If that mental model is incomplete — and without domain inquiry it almost certainly is — the tests are an incomplete specification, confidently asserted as complete. This is worse than no tests. It is false assurance.&lt;/p&gt;

&lt;p&gt;The cure is not better tests. It is understanding deep enough that the test's contribution becomes marginal. If the code expresses the domain correctly and reads plainly enough for a domain expert to validate its structure, the test suite's role as documentation and safety net diminishes considerably. A tester who says his functional tests serve as documentation of the application is making an admission: the production code has failed at its most important job. Documentation belongs in the place where it is always current — in code that reads like the domain it represents.&lt;/p&gt;




&lt;h2&gt;
  
  
  When the Process Becomes the Bug
&lt;/h2&gt;

&lt;p&gt;There is a question worth asking of every engineering practice, every tool, every ceremony: is this the best choice for the production system, or is it the best choice for the process, the tooling, or trend compliance?&lt;/p&gt;

&lt;p&gt;The production system is the artifact that matters. Everything else — the sprint board, the Jira backlog, the test suite, the deployment pipeline, the architecture decision records — is support infrastructure. It exists to serve the production system. The moment any of it starts making decisions for the production system, the hierarchy has inverted. And it inverts constantly, quietly, and with complete institutional legitimacy.&lt;/p&gt;

&lt;p&gt;Nobody says "we are going to let Jira determine our engineering decisions." But when a five-minute bug fix gets put on the backlog because the process requires it, Jira just made an engineering decision. When a developer adds an abstraction layer to satisfy a test framework rather than to express the domain, the test suite just shaped the production system. When a simple piece of logic gets restructured to comply with a framework convention that has no business relevance, trend compliance just overrode domain clarity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Process thinking asks:&lt;/strong&gt; are we following the process correctly? &lt;strong&gt;Production thinking asks:&lt;/strong&gt; what is the best outcome for the system?&lt;/p&gt;

&lt;p&gt;When they conflict, the answer should be immediate and unambiguous: the production system wins. The process is a tool. Tools do not have votes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Bug Economics
&lt;/h3&gt;

&lt;p&gt;Consider the real cost of a simple bug — a button that doesn't work, an enum stored as an integer instead of a string — when it travels through a process-first system versus a production-first one.&lt;/p&gt;

&lt;p&gt;In a production-first system with simple, readable code and a CI pipeline that allows release at any time: the bug is reported, understood, fixed, and released the same day. Total engineering time: five minutes to fix, minutes to release. The user experiences a brief interruption and a same-day resolution.&lt;/p&gt;

&lt;p&gt;In a process-first system the same bug looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Reported and logged: 10 minutes of administration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Discussed in standup or triage: 20 minutes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Estimated and planned into a sprint: 15 minutes in a planning meeting&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Picked up one or two sprints later by a developer who must first relearn the context, understand the bug, navigate the abstraction layers, fix the code, fix the broken tests, and write new tests: 60 minutes or more&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total: approximately 110 minutes of engineering time to resolve a 5-minute problem, with the user waiting six weeks for a fix that was always trivial. That is a 22-times cost multiplier applied entirely by the process. The bug is not better fixed. The system is not more stable. The outcome is strictly worse in every dimension — cost, speed, and user experience — and the process produced it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Kaizen Parallel
&lt;/h3&gt;

&lt;p&gt;This is not a new insight. Toyota's lean manufacturing principles identified this failure mode decades ago under the concept of &lt;em&gt;muda&lt;/em&gt; — waste. Waste in production systems is any activity that consumes resources without adding value. The 105 minutes of process overhead on a 5-minute fix is almost pure waste: motion without value, waiting, unnecessary processing.&lt;/p&gt;

&lt;p&gt;The deeper Kaizen principle is that the person closest to the problem is best positioned to fix it. The developer who wrote the code, who understands it today, who can see the bug clearly right now — that person fixing it immediately is the optimal outcome by every measure. Deferring it transfers the problem to a different person at a different time with less context, more overhead, and a worse result.&lt;/p&gt;

&lt;p&gt;Empirically, this approach does not produce more bugs. Teams that have observed both models report comparable defect rates. The difference is resolution time: same-day fixes versus multi-sprint delays. On the metric that actually matters to the business — how long does a known problem affect users — the simple, production-first system wins decisively.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Job
&lt;/h2&gt;

&lt;p&gt;The assembly part of software development — implementing a described behaviour to pass a set of tests — is a commodity skill. It is increasingly automatable. It produces measurable output in a sprint and moves tickets across a board. It is the part of the job that process-first thinking measures, rewards, and optimises for.&lt;/p&gt;

&lt;p&gt;The understanding part is not a commodity. It is not automatable. It does not show up in velocity metrics or test coverage percentages. But it is the part that determines whether the software is actually correct. It is the part that finds the circular ownership scenario before it becomes a compliance incident. It is the part that knows what to leave out. It is the part that produces code readable enough that a domain expert can spot an error without running a test. It is the part that makes a bug a five-minute fix rather than a two-sprint project. And it is the part that occasionally recognises that the problem as described is a symptom — and builds the thing that makes the symptom impossible.&lt;/p&gt;

&lt;p&gt;Everything that is not in direct service of the production system is not neutral overhead today. It is an obstacle tomorrow. The fifteen-year lifespan makes this visible in a way that a two-year project never does. The complexity accumulates. The process overhead compounds. The abstractions added for testability become the walls that trap the system. The dependencies added for framework compliance become the liabilities that prevent the upgrade. The architectural mandates applied without domain justification become the constraints that make every change expensive.&lt;/p&gt;

&lt;p&gt;Ask of every decision: is this the best choice for the production system? If the honest answer is "no, but it satisfies the process" — remove it. Whatever is not there cannot break, does not need maintenance, and does not need to be understood.&lt;/p&gt;

&lt;p&gt;Simplicity is not the absence of effort. It is the result of understanding deep enough to know what to remove.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The properties described in this article — longevity, upgradeability, maintainability, extensibility, readability, and organisation — are not independent qualities to be optimised separately. They are consequences of a single discipline: understanding the domain well enough to represent it simply, correctly, and durably in code that will outlast the people who wrote it. The process serves that goal. When it stops serving that goal, the process is the bug.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>softwareengineering</category>
      <category>devops</category>
      <category>java</category>
      <category>architecture</category>
    </item>
    <item>
      <title>What Is a Rich Domain Model?</title>
      <dc:creator>Leon Pennings</dc:creator>
      <pubDate>Tue, 19 May 2026 11:48:05 +0000</pubDate>
      <link>https://dev.to/leonpennings/what-is-a-rich-domain-model-2d0k</link>
      <guid>https://dev.to/leonpennings/what-is-a-rich-domain-model-2d0k</guid>
      <description>&lt;p&gt;Most articles about rich domain models get lost in comparisons to anemic models, debates about OOP mechanics, or pattern catalogues. This is not one of those articles.&lt;/p&gt;

&lt;p&gt;A rich domain model is not a technical pattern. It is a discipline — one that produces a living, explicit representation of the essential complexity of a business domain. Understanding what that means, and what it unlocks, requires stepping back from the code entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Essential Complexity, Made Explicit
&lt;/h2&gt;

&lt;p&gt;Start with what a rich domain model actually is.&lt;/p&gt;

&lt;p&gt;It is a set of objects, each playing a defined role in the business domain, each owning the responsibility that role entails. Not what state they carry — but what they know, what they decide, and what belongs to them. Think of it less like a data structure and more like a cast of actors: each one has a role, and the role defines everything. What they are responsible for. What they know. What they act on. What they refuse.&lt;/p&gt;

&lt;p&gt;This distinction matters more than it might seem. An actor on stage is not described by listing their costume and props. They are described by their role — what they do, what they own, what they are accountable for. The props are incidental. In the same way, a domain object is not defined by the fields it holds. It is defined by its responsibility. State may be part of how it fulfills that responsibility — but it is an implementation detail of the role, not the definition of it.&lt;/p&gt;

&lt;p&gt;The contrast with an anemic model follows directly. An anemic model is a cast of actors who have been stripped of their roles. They stand on stage holding props while someone offstage calls out instructions. The data is visible. The knowledge of what to do with it is gone — moved into service classes, transaction scripts, and workflow configurations that grow without principle and conflict without resolution.&lt;/p&gt;

&lt;p&gt;Fred Brooks gave us the vocabulary to understand why this matters. He distinguished between &lt;em&gt;essential complexity&lt;/em&gt; — the complexity intrinsic to the problem itself, which cannot be removed — and &lt;em&gt;accidental complexity&lt;/em&gt;, everything else: the frameworks, the indirections, the patterns applied without cause.&lt;/p&gt;

&lt;p&gt;The actors and their roles &lt;em&gt;are&lt;/em&gt; the essential complexity. They are not a representation of it or a metaphor for it — they are it, made visible and explicit. Every business rule that is genuinely hard, every lifecycle that has real consequences, every constraint that exists because the business demands it: these find their home in a role, owned by an actor, named and present in the model. You can see the essential complexity. You can point to it. You can reason about it directly.&lt;/p&gt;

&lt;p&gt;Once the essential complexity is that explicit, accidental complexity loses its camouflage. It cannot pretend to belong. Every framework choice, every infrastructure decision, every pattern applied can be held up against a simple question: does a domain object — a named actor with a defined role — actually require this? If not, it is accidental complexity, and it has no business being there. The model makes that judgment possible because the essential complexity is no longer hiding.&lt;/p&gt;

&lt;p&gt;This is not the same as reducing complexity. The business is as complex as it is. What changes is whether that complexity is visible, owned, and honest — or scattered, implicit, and discovered only when things break. The rich domain model ensures the essential complexity is always primary. Everything else is secondary, and known to be so.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Tool for Learning the Domain
&lt;/h2&gt;

&lt;p&gt;Most development approaches start from requirements. A user story describes motion through a system: a user does something, something happens. This teaches you the rivers — the flows, the happy paths, the scenarios that have been thought of so far.&lt;/p&gt;

&lt;p&gt;A domain model teaches you the terrain. Once you understand the terrain, the rivers make sense. Without it, you are always following water, never knowing where you are.&lt;/p&gt;

&lt;p&gt;This distinction matters enormously in practice. When a developer learns a business domain through user stories and debugging, they accumulate procedural knowledge. They learn symptoms. They build a mental model that is a patchwork of scenarios, edge cases, and tribal knowledge. That understanding does not transfer easily and does not survive personnel changes.&lt;/p&gt;

&lt;p&gt;When a developer learns through the domain model — starting with the core concepts, understanding their responsibilities and relationships — they learn causes. The &lt;em&gt;what&lt;/em&gt; and &lt;em&gt;why&lt;/em&gt; of the business becomes clear before the &lt;em&gt;how&lt;/em&gt;. Onboarding that previously took months can take hours, not because the business became simpler, but because its essential structure was made explicit and navigable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Canonical Truth for the Business Domain
&lt;/h2&gt;

&lt;p&gt;A codebase without a domain model has no authoritative reference for what the business believes. Logic accumulates in transaction scripts, in service classes, in stored procedures, in workflow configurations. It is never gathered in one place where you can ask: is this consistent? Does this conflict with that?&lt;/p&gt;

&lt;p&gt;The rich domain model is that place. It is not documentation in the sense of comments or wikis — those go stale and lie. It is living documentation, expressed in code, that is wrong only when the code is wrong. When two features conflict, the domain model is the referee. When a new requirement arrives, the model is the context in which it is evaluated — is this already expressed somewhere? Does this contradict something that exists?&lt;/p&gt;

&lt;p&gt;Without that context, conflicting logic does not just happen occasionally. It is inevitable. There is no shared reference, so there is no way to prevent divergence. The model prevents it not through process or discipline, but through the simple fact of existing.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Belongs in the Domain Model
&lt;/h2&gt;

&lt;p&gt;A common misconception is that domain objects are database rows dressed up with methods. This conflation produces models that are anemic by construction — the shape of the schema becomes the shape of the domain, and the domain becomes a mirror of the persistence layer rather than a representation of the business.&lt;/p&gt;

&lt;p&gt;The domain object is defined by its &lt;em&gt;responsibility&lt;/em&gt;, not by its persistence. Whether it holds state is irrelevant to whether it belongs in the model. What matters is whether it represents a genuine business concept with a defined responsibility.&lt;/p&gt;

&lt;p&gt;Some domain objects have state that should be persisted. In that case, the ORM annotations live on the domain object itself — there is no separate entity class, no parallel representation. The domain object is the single source of truth, and persistence is simply a capability some objects happen to have. There is no ORM object that is not a domain object. If one exists, that is the smell — not a feature. Some will object that this violates persistence ignorance — that the domain should not know about its own storage. But a domain object declaring what it needs is not pollution. It is honesty. The alternative — a parallel entity class that mirrors the domain object field by field — is not cleaner architecture. It is the same information written twice, with an extra layer of indirection between them and nothing gained in return.&lt;/p&gt;

&lt;p&gt;Other domain objects have no persistent state at all. A &lt;code&gt;CurrencyConversion&lt;/code&gt; that owns the rules and cache for converting between currencies is a full citizen of the domain model. An &lt;code&gt;Interaction&lt;/code&gt; that represents a session of intent against the domain — carrying the current user, the transaction boundary, the active roles — is a domain object. Neither has a table. Both have clear, defined responsibilities.&lt;/p&gt;

&lt;p&gt;The question is never "does this have a table?" The question is always "does this represent something real in the business, with a responsibility that can be named?"&lt;/p&gt;




&lt;h2&gt;
  
  
  The Interaction: A Worked Example
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Interaction&lt;/code&gt; deserves particular attention because it illustrates what correct modeling unlocks beyond the obvious.&lt;/p&gt;

&lt;p&gt;Every non-trivial business application has the concept of an interaction: a moment of intent against the domain, initiated by a known user, within a defined transactional boundary, with a lifecycle that has a beginning and an end. This concept exists whether you model it or not. The question is whether it is explicit or scattered across framework configuration, security filters, transaction annotations, and audit log scrapers.&lt;/p&gt;

&lt;p&gt;When modeled explicitly — made available within the execution context, whether via &lt;code&gt;ThreadLocal&lt;/code&gt;, scoped storage, or whatever the runtime demands — &lt;code&gt;Interaction&lt;/code&gt; becomes the natural owner of everything that belongs to that lifecycle. The storage mechanism is an implementation detail. The concept is not.&lt;/p&gt;

&lt;p&gt;During an interaction, any part of the domain can ask &lt;code&gt;Interaction.hasUserRole(CancelOrderRole.class)&lt;/code&gt; — not as a security check imposed from outside, but as a domain question answered where the action is performed. Authentication is resolved before the interaction begins; a valid &lt;code&gt;Interaction&lt;/code&gt; means a valid user. Authorization is expressed where it is enforced.&lt;/p&gt;

&lt;p&gt;At the end of an interaction, deferred actions execute within the same transactional boundary. Emails are sent, events are fired, downstream reactions trigger — and if anything fails, everything rolls back, including the email that had not yet been sent. This guarantee is structurally impossible to achieve with a message broker bolted onto the outside of an application without significant infrastructure overhead. Here it is a natural consequence of the model.&lt;/p&gt;

&lt;p&gt;After the end of an interaction, post-transaction actions execute outside the boundary, intentionally and explicitly. On cleanup, state is torn down predictably — no leaked state between requests.&lt;/p&gt;

&lt;p&gt;The audit trail — who did what, when, and did it succeed — emerges naturally because &lt;code&gt;Interaction&lt;/code&gt; already knows all of it. It is not assembled from logs after the fact.&lt;/p&gt;

&lt;p&gt;None of these capabilities were designed individually. They are all consequences of modeling the right concept. This is what essential complexity, made explicit, produces.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Order: Lifecycle as Domain Responsibility
&lt;/h2&gt;

&lt;p&gt;The same principle applies to any object with a meaningful lifecycle. Consider an &lt;code&gt;Order&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Order&lt;/code&gt; is constructed from an &lt;code&gt;OrderRequest&lt;/code&gt;. In its constructor — or through an &lt;code&gt;assemble()&lt;/code&gt; method called immediately — it validates that all items have prices (failing fast if not), reserves inventory, creates the &lt;code&gt;Invoice&lt;/code&gt;, determines from the request whether fulfillment is pickup or delivery, and if delivery, creates the &lt;code&gt;Shipment&lt;/code&gt; internally. No external coordinator performs these steps. The &lt;code&gt;Order&lt;/code&gt; knows what it means to be an order.&lt;/p&gt;

&lt;p&gt;Once assembled, the &lt;code&gt;Order&lt;/code&gt;'s state gates what is possible. &lt;code&gt;deliver()&lt;/code&gt; is only reachable because &lt;code&gt;assemble()&lt;/code&gt; completed. Anything attached to an order — documents, notes, events — is evaluated against the current state. The object enforces its own rules.&lt;/p&gt;

&lt;p&gt;The lifecycle of the order is expressed in &lt;code&gt;OrderMilestone&lt;/code&gt; objects: &lt;code&gt;created&lt;/code&gt; at &lt;code&gt;LocalDateTime&lt;/code&gt; X, &lt;code&gt;ItemsCompleted&lt;/code&gt; at X+1, &lt;code&gt;Shipped&lt;/code&gt; at X+2. This is not logging in the developer sense. This is the &lt;code&gt;Order&lt;/code&gt; remembering its own history. Audit trails, reporting, and debugging are free consequences of a model that is honest about time.&lt;/p&gt;

&lt;p&gt;There is no &lt;code&gt;OrderService&lt;/code&gt; that knows the steps. There is no &lt;code&gt;OrderProcessor&lt;/code&gt; that coordinates the flow. What is often called orchestration is simply the &lt;code&gt;Order&lt;/code&gt;'s own behavior, waiting to be claimed.&lt;/p&gt;




&lt;h2&gt;
  
  
  There Is No Such Thing as Orchestration
&lt;/h2&gt;

&lt;p&gt;"Orchestration" is a concept that appears when objects are not carrying enough responsibility. The argument is that some flows are too complex to live in any single object, that something external must coordinate. But this argument always rests on the same foundation: the objects being coordinated are anemic. They cannot coordinate themselves because they hold no behavior.&lt;/p&gt;

&lt;p&gt;The stronger claim is this: orchestration is a business process, and every business process has an owner. The moment you ask "whose responsibility is this flow?" the answer is always a named thing in the business. Named things in the business belong in the domain model.&lt;/p&gt;

&lt;p&gt;If the checkout flow belongs to &lt;code&gt;Order&lt;/code&gt;, there is no orchestration — only an object doing its job. If a more complex cross-domain process exists, the business has a name for it. That name is your object.&lt;/p&gt;

&lt;p&gt;The workflow engine question resolves the same way. A workflow engine is infrastructure for implementing an unmodelled requirement. It allows a business process to be encoded without ever being understood. The process runs, tickets close, and the pressure to model never arrives. Meanwhile the process becomes invisible — it lives in configuration, not in the domain, and the model no longer reflects reality.&lt;/p&gt;

&lt;p&gt;By making the process explicit in the model, you force the understanding upfront. Traceability, accountability, and auditability are not bolted on afterward — they are natural consequences of a process that is owned and expressed. And the model becomes resistant to casual change. A workflow engine can be reconfigured quietly. A domain object that explicitly models a process requires intentional change. You must touch the model. That is not a constraint — it is a feature.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture That Emerges
&lt;/h2&gt;

&lt;p&gt;When the domain model is honest and complete, the architecture that surrounds it becomes remarkably simple.&lt;/p&gt;

&lt;p&gt;The domain is the center. Everything else is translation. An adapter takes an external signal — an HTTP request, a queue message, a UI event, a file drop — translates it into something the domain understands, and translates the response back. Whether that adapter is called a web service, a UI connector, or a queue client is an implementation detail. Its functional purpose is always the same: adapt an external request to the domain, and an answer from the domain to the outside world.&lt;/p&gt;

&lt;p&gt;This framing eliminates the need for many patterns that exist only because the domain is not carrying its weight. There is no need for a dependency injection container to wire together a domain that is self-contained. There is no need for a repository pattern when persistence is an annotation on the domain object that requires it. There is no layered architecture to enforce when the boundary between domain and adapter is conceptual and obvious.&lt;/p&gt;

&lt;p&gt;The complexity budget is spent entirely on essential complexity, because there is nowhere for accidental complexity to hide. Every technology choice can be evaluated against a single question: does a domain object require this? If not, it has no business being there. The domain model is not just a design tool — it is the justifier for every architectural decision, the brake on over-engineering, and the answer to YAGNI grounded not in gut feel but in domain reasoning.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Principle Underneath
&lt;/h2&gt;

&lt;p&gt;There is a principle that connects everything above:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The ease of implementing something without modeling it is proportional to the hidden cost of never having modeled it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every approach that starts from &lt;em&gt;how&lt;/em&gt; rather than &lt;em&gt;what&lt;/em&gt; — procedural scripts, transaction-script architectures, use-case driven development — shares this characteristic. The requirement is the input, the implementation is the output, and the domain never appears. Each new requirement starts from scratch, because there is no accumulated understanding to build on. The codebase grows. The knowledge does not.&lt;/p&gt;

&lt;p&gt;A rich domain model inverts this entirely. The domain is the input. Requirements are queries against that understanding. New requirements find their place in something that already exists — or reveal, through the friction of not fitting, that the domain needs to grow. Either way, understanding accumulates. The model becomes more true over time, not less.&lt;/p&gt;

&lt;p&gt;That is what a rich domain model is. Not a pattern. Not a layer. A discipline of making the essential complexity of a business explicit, owned, and honest — and letting everything else follow from that.&lt;/p&gt;







&lt;h2&gt;
  
  
  Sidebar: On AI-Assisted Development
&lt;/h2&gt;

&lt;p&gt;AI is genuinely useful in a domain-centric codebase — for implementing adapters, generating boilerplate, and accelerating everything that surrounds the model. It pattern-matches well against known structures, and once the domain is understood, there is plenty of that work to do.&lt;/p&gt;

&lt;p&gt;Domain modeling is a different activity. It requires understanding what the business actually is — not just what a ticket describes. It requires recognizing when a concept is missing, resisting the obvious implementation in favor of the correct abstraction, and making judgment calls about responsibility that have no objectively correct answer. AI has no access to the lived understanding that produces those judgments.&lt;/p&gt;

&lt;p&gt;The most useful role for AI in a modeling context is as a mirror — a Socratic partner for stress-testing a hypothesis about a concept's responsibility or boundary. It surfaces objections, identifies gaps, and forces precision. That is valuable. But the modeling itself remains a human activity, and the discipline of doing it remains more important in an AI-assisted world, not less. Without the model, AI produces procedural code at unprecedented speed — and accumulates the hidden cost of unmodeled requirements faster than any previous approach.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sidebar: On Practical Effects
&lt;/h2&gt;

&lt;p&gt;The common perception is that a rich domain model requires heavy upfront investment — that you must design everything before writing any code, and that this slows delivery. In practice the opposite is true, and the gap becomes visible quickly.&lt;/p&gt;

&lt;p&gt;Early in a project, a team building a rich domain model is establishing core concepts and their responsibilities. This feels slower than a team wiring up framework configuration and generating boilerplate. But by the time the first meaningful features are being built, the domain team is adding behavior to objects that already understand the business. New requirements find their place. The model tells you where things belong. The other team is asking "where does this code go?" for every new feature — and the answers are becoming less consistent, not more.&lt;/p&gt;

&lt;p&gt;The acceleration compounds. Maintenance is cheaper because the model is the documentation — it cannot go stale, because it is the code. Debugging is faster because the model expresses business intent, not just technical state. The difference between "the Order refused shipment because it was already delivered" and "some process node returned an unexpected status" is the difference between understanding and archaeology.&lt;/p&gt;

&lt;p&gt;The people costs tell the same story. Onboarding a developer onto a well-modeled domain takes hours, not months. The knowledge is in the model, not in the heads of the people who built it. That is not just an efficiency gain — it is a risk reduction. The bus factor of an application with an explicit domain model is structurally higher than one without.&lt;/p&gt;

&lt;p&gt;The cost of not modeling is real, large, and almost never measured — because there is no comparable version of the same application where it was modeled. You cannot see the cost of understanding you never accumulated. You only feel it, gradually, in every feature that takes longer than it should, every bug that touches more than it should, and every developer who leaves taking knowledge that was never made explicit.&lt;/p&gt;







&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;p&gt;This article is part of a series on domain-centric thinking.&lt;/p&gt;

&lt;p&gt;If this raised the question of &lt;em&gt;how to start modeling&lt;/em&gt; — how to discover the actors, assign the roles, and run a discovery session before a line of code is written — that is covered in &lt;a href="https://blog.leonpennings.com/rich-domain-models-start-with-what-is-not-what-happens" rel="noopener noreferrer"&gt;[Rich Domain Models: Start with What Is, Not What Happens]&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to see a domain model grow through concrete examples — how a real model evolves as understanding deepens, and what it means to let a new requirement reshape the model rather than just add to it — that is covered in &lt;a href="https://blog.leonpennings.com/rich-domain-modelling-a-library-story" rel="noopener noreferrer"&gt;[Rich Domain Models: A Library Story]&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The concepts in this article reflect practical experience building domain-centric applications. The&lt;/em&gt; &lt;code&gt;Interaction&lt;/code&gt; &lt;em&gt;pattern described has been in production use since 2009.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ddd</category>
      <category>softwaredevelopment</category>
      <category>java</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
