<?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>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>
    <item>
      <title>The Architecture Tax — Why Enterprise Software Is Expensive, and Why AI Won't Fix It</title>
      <dc:creator>Leon Pennings</dc:creator>
      <pubDate>Mon, 18 May 2026 07:38:59 +0000</pubDate>
      <link>https://dev.to/leonpennings/the-architecture-tax-why-enterprise-software-is-expensive-and-why-ai-wont-fix-it-57ek</link>
      <guid>https://dev.to/leonpennings/the-architecture-tax-why-enterprise-software-is-expensive-and-why-ai-wont-fix-it-57ek</guid>
      <description>&lt;h2&gt;
  
  
  The story the industry tells
&lt;/h2&gt;

&lt;p&gt;Enterprise software is expensive. It requires large teams, significant infrastructure, complex deployment pipelines, and sustained operational effort. Requirements that sound simple take weeks. Systems that should be stable require constant attention. The codebase that was coherent at year one is opaque by year four. New developers take months to become productive. Changes that touch multiple parts of the system require coordination that absorbs more time than the implementation itself.&lt;/p&gt;

&lt;p&gt;This is treated as a given. Enterprise software is complex, therefore it costs what it costs. The architecture — microservices, distributed infrastructure, containerised deployments, orchestration layers — is presented as the response to that complexity. Sophisticated problems require sophisticated solutions.&lt;/p&gt;

&lt;p&gt;The argument this article makes is the opposite.&lt;/p&gt;

&lt;p&gt;Most of what the industry calls the cost of enterprise software is not the cost of the domain. It is the cost of workarounds for a missing domain model — compounded over years, normalised by the fact that every team around you is paying the same price and calling it inevitable. The architecture is not the response to the complexity. In most cases, it is the cause of it.&lt;/p&gt;

&lt;p&gt;And the reason this remains invisible is that the alternative was never built. You cannot compare your system to the system that does not exist. So the costs accumulate, get attributed to the nature of enterprise software, and become the baseline against which all future decisions are made.&lt;/p&gt;

&lt;p&gt;This article is about what is actually in that price tag, and what it would cost without it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The context problem
&lt;/h2&gt;

&lt;p&gt;When a team starts building a system, the code is small. The domain is not yet fully understood, but the surface area is manageable. A developer can hold the whole thing in their head. A new feature means adding a function. The system works. Nobody is in pain.&lt;/p&gt;

&lt;p&gt;Three years later, the same team — or more likely, a partially replaced team — is asking a different question. Not "does this work?" but "where does this live?" Where does the discount calculation happen? Who owns the rule that a cancelled order cannot be reinstated after shipment? If we change how rush orders are priced, how many places do we need to touch, and how many of those will we miss?&lt;/p&gt;

&lt;p&gt;These are not questions about the business domain. The business domain has not become harder. An order is still an order. The questions are about the system — specifically, about where the system chose to put things, and whether that choice was made deliberately or simply accumulated over time.&lt;/p&gt;

&lt;p&gt;This is the context problem. It is the root cause of most of the complexity that teams eventually reach for distributed architectures to solve. And it has nothing to do with the scale or ambition of the domain. It is a structural property of how the code was organised from the beginning.&lt;/p&gt;

&lt;p&gt;Context, in the sense used here, has a specific meaning. It is not a folder, a module name, or a service boundary. It is the answer to a structural question: &lt;em&gt;given a concept in the domain, is there one authoritative location where all rules governing that concept are defined and enforced?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A concept has a context when the answer is yes. It does not have a context when the answer is "it depends" — or "mostly here, but also there, and that other place handles the exception."&lt;/p&gt;

&lt;p&gt;The distinction matters because systems do not stay small. Rules accumulate. Exceptions are added. Behaviour that was simple in year one becomes conditional in year two and contradictory in year three. In a system with clear context ownership, that accumulation is manageable — the rules are in one place, contradiction is visible, and the design either holds or signals clearly that it needs to change. In a system without context ownership, accumulation is invisible until it becomes crisis.&lt;/p&gt;




&lt;h2&gt;
  
  
  Object orientation was supposed to solve this
&lt;/h2&gt;

&lt;p&gt;The context problem is not new. It is precisely the problem that object-oriented programming was designed to address.&lt;/p&gt;

&lt;p&gt;Object orientation, in its original conception, was not about classes, inheritance hierarchies, or design patterns. It was about a single structural idea: that data and the rules governing that data belong together, in one place, unreachable from outside except through defined behaviour. An object is not a container for data with methods attached. It is a context — a thing that knows its own state, enforces its own rules, and decides what to do when asked. The outside world cannot manipulate its internals. It can only send messages.&lt;/p&gt;

&lt;p&gt;This is context ownership as a structural property of the code. Logic cannot drift to wherever it is convenient to put it, because the object's state is private. The rule that a shipped order cannot be cancelled does not live in a service method that someone has to know to call. It lives on the order itself, enforced by the fact that the order's status cannot be changed except through the order's own behaviour. It is not a convention. It is a constraint.&lt;/p&gt;

&lt;p&gt;This is what object orientation was for.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Java enterprise actually practises
&lt;/h2&gt;

&lt;p&gt;The dominant pattern in Java enterprise development — and in enterprise development more broadly — looks like this:&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;Order&lt;/code&gt; entity holds fields annotated for persistence. Its fields are private, which gives the appearance of encapsulation. An &lt;code&gt;OrderService&lt;/code&gt; contains the business logic — the methods that create, modify, and query orders. An &lt;code&gt;OrderRepository&lt;/code&gt; handles the database interaction. Data transfer objects carry information between layers.&lt;/p&gt;

&lt;p&gt;This pattern is widely understood to be object-oriented. It uses objects. It has private fields. It has classes with clear names and single responsibilities. Senior developers teach it. Frameworks are built around it. It is the default.&lt;/p&gt;

&lt;p&gt;It is procedural programming.&lt;/p&gt;

&lt;p&gt;The test is not whether the code uses classes. The test is whether data and the rules governing that data are in the same place. In the service-DTO-repository pattern, they are not. The &lt;code&gt;Order&lt;/code&gt; entity holds data. The &lt;code&gt;OrderService&lt;/code&gt; holds logic. The logic is separated from the data it governs. That is the definition of procedural code — regardless of the language, regardless of the annotations, regardless of the private keyword on the fields.&lt;/p&gt;

&lt;p&gt;The private fields are not encapsulation in any meaningful sense. Encapsulation means the object protects its own invariants. Nothing outside can put it in an invalid state. But if &lt;code&gt;OrderService&lt;/code&gt; loads an &lt;code&gt;Order&lt;/code&gt;, inspects its fields, and decides what to do — the private keyword is decoration. The order is a struct. The service is a function that operates on it. The fact that both are expressed as classes changes nothing about the structure.&lt;/p&gt;

&lt;p&gt;A senior developer once described object orientation as "just using a lot of objects." In the Spring ecosystem, that description is accidentally accurate. The objects are present. The orientation — the structural commitment to context ownership — is not.&lt;/p&gt;

&lt;p&gt;This matters because it means most teams believe they are already doing what a rich domain model offers. The gap between what they believe and what is actually true is where the context problem silently grows — invisible, until it becomes the thing that makes the system expensive.&lt;/p&gt;




&lt;h2&gt;
  
  
  How a procedural system rots
&lt;/h2&gt;

&lt;p&gt;The rot does not happen at once. It has a characteristic progression that is worth tracing, because understanding the mechanism is what makes the solution legible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Year one.&lt;/strong&gt; The system is small. The team is mostly the original team. The rules fit in one or two services. &lt;code&gt;OrderService&lt;/code&gt; is coherent because it is young and the domain is still understood by everyone who touches it. Velocity is high. The architecture feels like a good decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Year two.&lt;/strong&gt; The product grows. New rules are added. The team adds members who know the services they own but not the full picture. A pricing exception is added in &lt;code&gt;OrderService&lt;/code&gt; because that is where the original pricing logic lives. A second exception is added in &lt;code&gt;PricingService&lt;/code&gt; because by then the first developer has left and the new one reasonably concluded that pricing rules belong in the pricing service. Both are correct by local reasoning. Neither is aware of the other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Year three.&lt;/strong&gt; The team is running two integration tests that cover the same scenario and produce different results depending on which code path is invoked. A bug report arrives: under certain conditions, the price shown to the customer differs from the price on the invoice. Three services are involved in producing those two numbers. The fix requires coordinating changes across all three, understanding the original intent of logic nobody wrote, and ensuring that the correction does not break the scenarios the divergent logic was accidentally handling correctly.&lt;/p&gt;

&lt;p&gt;This is not a failure of discipline. The developers are competent. It is the structural consequence of a system that provided no home for rules — so rules went wherever they were needed, and the system slowly became a map of historical decisions rather than a coherent model of the domain.&lt;/p&gt;

&lt;p&gt;No amount of discipline permanently solves a structural problem. Discipline degrades over time and team turnover. Structure does not.&lt;/p&gt;




&lt;h2&gt;
  
  
  The rich domain model as structural answer
&lt;/h2&gt;

&lt;p&gt;A rich domain model addresses the context problem through structure, not discipline.&lt;/p&gt;

&lt;p&gt;The principle is simple: an object owns the rules that govern its own state, and state changes happen only through that object's behaviour. An &lt;code&gt;Order&lt;/code&gt; does not have its price calculated by a service. An &lt;code&gt;Order&lt;/code&gt; knows its price — it is a property of the order, derived from the order's own data and the rules encoded in the order's own methods. The service does not reach in and manipulate the order's internals. It asks the order to do something, and the order either does it according to its rules, or refuses.&lt;/p&gt;

&lt;p&gt;Consider an order system modelled this way:&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;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&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;OrderLine&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lines&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="nc"&gt;OrderStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;ShippingMethod&lt;/span&gt; &lt;span class="n"&gt;shippingMethod&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;Money&lt;/span&gt; &lt;span class="nf"&gt;calculatePrice&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Money&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lines&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;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;OrderLine:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;lineTotal&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reduce&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ZERO&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;Money:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;add&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;shippingMethod&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;applyTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&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="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;confirm&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DRAFT&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalStateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Only draft orders can be confirmed."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CONFIRMED&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="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SHIPPED&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalStateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Shipped orders cannot be cancelled."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CANCELLED&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;The rule that a shipped order cannot be cancelled lives on the &lt;code&gt;Order&lt;/code&gt;. Not in &lt;code&gt;OrderService&lt;/code&gt;, not in a validator upstream, not in a flag checked somewhere in the call chain. It lives in the only place it could coherently live: the object that owns the concept. A developer three years from now, touching this code for the first time, cannot accidentally bypass that rule — not because the system trusts their discipline, but because the structure does not give them a way to.&lt;/p&gt;

&lt;p&gt;The service that orchestrates this is correspondingly simple:&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="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;OrderConfirmation&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reserve&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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OrderConfirmation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database transaction is the failure boundary. If anything fails, nothing happened. There are no compensating calls, no saga steps, no partial states to reconcile. The infrastructure serves the domain. The domain is not distorted to accommodate the infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design pressure as a feature
&lt;/h2&gt;

&lt;p&gt;There is a property of the rich domain model that is easy to overlook: it makes bad design visible before it becomes operational pain.&lt;/p&gt;

&lt;p&gt;When a new rule is added that does not fit cleanly — when a developer sits down to implement something and cannot find a natural home for it in the model — that is not an inconvenience. It is a signal. The model is telling you that either the rule is being misunderstood, or the model needs to evolve to accommodate a concept it does not yet represent.&lt;/p&gt;

&lt;p&gt;In a procedural system, that signal does not fire. The developer adds a condition to an existing service method, or adds a new service if the feature is large enough. The rule is implemented. It works. The fact that it created divergence from an existing rule, or that it sits awkwardly between two existing concepts, is not visible until months later when something breaks in a way that requires archaeology to understand.&lt;/p&gt;

&lt;p&gt;The rich model converts architectural drift from a silent accumulation into an explicit design question. That question is not always comfortable. But discomfort at design time costs a discussion. Discomfort at runtime costs an incident.&lt;/p&gt;




&lt;h2&gt;
  
  
  The business changed. As it always does.
&lt;/h2&gt;

&lt;p&gt;The system above handles standard orders. The domain is coherent. The rules are clear. Now the business introduces a new requirement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rush orders.&lt;/strong&gt; A customer can request expedited fulfilment. This attracts a surcharge — the order price increases by fifteen percent, and the shipping method is upgraded to express.&lt;/p&gt;

&lt;p&gt;In a procedural system, this requires touching multiple places. The pricing calculation needs a condition. The shipping assignment needs a condition. If those live in different services, both need to change, both need to be deployed, and the rule "rush orders cost fifteen percent more and ship express" exists nowhere as a statement. It exists as a set of conditional branches distributed across the system.&lt;/p&gt;

&lt;p&gt;In the rich domain model, the question the implementation forces you to answer is: &lt;em&gt;what is a rush order?&lt;/em&gt; Is it a type of order? A property? Does it affect the order itself or its fulfilment? Answering that question is the design. And the answer produces something like:&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;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&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;OrderLine&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lines&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="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;rush&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;ShippingMethod&lt;/span&gt; &lt;span class="n"&gt;shippingMethod&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lines&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;this&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="n"&gt;request&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="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rush&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isRush&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;shippingMethod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rush&lt;/span&gt;
            &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;ShippingMethod&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;EXPRESS&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ShippingMethod&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;STANDARD&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;Money&lt;/span&gt; &lt;span class="nf"&gt;calculatePrice&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Money&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lines&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;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;OrderLine:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;lineTotal&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reduce&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ZERO&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;Money:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;Money&lt;/span&gt; &lt;span class="n"&gt;withShipping&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shippingMethod&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;applyTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&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;rush&lt;/span&gt;
            &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;withShipping&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;multiplyBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.15&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;withShipping&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;The rule lives on the &lt;code&gt;Order&lt;/code&gt;. It cannot live anywhere else. Every developer who touches order pricing in the future will find it here, because there is only one place to look.&lt;/p&gt;




&lt;h2&gt;
  
  
  The business changed again.
&lt;/h2&gt;

&lt;p&gt;Three weeks after the rush order feature ships, a new requirement arrives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VIP customers do not pay the rush surcharge.&lt;/strong&gt; The expedited shipping still applies — VIPs get the faster fulfilment — but the fifteen percent price increase is waived as a benefit of their status.&lt;/p&gt;

&lt;p&gt;This requirement is three sentences of business logic. What it does to a system without context ownership is disproportionate to its size.&lt;/p&gt;

&lt;p&gt;In a procedural system, the question is: &lt;em&gt;where does this condition go?&lt;/em&gt; The rush surcharge is currently in — actually, let us retrace that. The original pricing was in &lt;code&gt;OrderService&lt;/code&gt;. The rush surcharge was added in &lt;code&gt;PricingService&lt;/code&gt; because that seemed more appropriate for a pricing concern. The VIP status lives in &lt;code&gt;CustomerService&lt;/code&gt;. A rule that says "apply the surcharge unless the customer is a VIP" now requires either a call from &lt;code&gt;PricingService&lt;/code&gt; to &lt;code&gt;CustomerService&lt;/code&gt; — coupling two services that were not coupled before — or an orchestration layer that assembles the inputs before calling either, or a flag passed through the call chain from wherever the customer is known to wherever the pricing happens, leaking context across layers that should not share it.&lt;/p&gt;

&lt;p&gt;Each of these is a workaround. Each adds a seam. And each seam is a place where, two years from now, someone adds another condition, and the question "what does this order actually cost?" requires reading four services to answer.&lt;/p&gt;

&lt;p&gt;In the rich domain model, the question is different and better: &lt;em&gt;who owns the rule that VIP customers are exempt from the rush surcharge?&lt;/em&gt; Is it the Order? The Customer? A pricing policy?&lt;/p&gt;

&lt;p&gt;This is a domain design question. It has a defensible answer:&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;public&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt; &lt;span class="nf"&gt;calculatePrice&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Money&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lines&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;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;OrderLine:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;lineTotal&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reduce&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ZERO&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;Money:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;Money&lt;/span&gt; &lt;span class="n"&gt;withShipping&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shippingMethod&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;applyTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rush&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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="na"&gt;isVip&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;withShipping&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;multiplyBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.15&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;withShipping&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;The rule is in one place. It reads as a statement of business intent. It is testable in isolation. When the next requirement arrives — "VIP customers also get free express shipping on rush orders over two hundred euros" — the developer knows exactly where to go, and the existing logic tells them exactly what the current rules are.&lt;/p&gt;

&lt;p&gt;If the pricing logic grows complex enough, the model signals it:&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;public&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt; &lt;span class="nf"&gt;calculatePrice&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Money&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lines&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;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;OrderLine:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;lineTotal&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reduce&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ZERO&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;Money:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;Money&lt;/span&gt; &lt;span class="n"&gt;withShipping&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shippingMethod&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;applyTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;PricingPolicy&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="na"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;withShipping&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&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;The complexity of &lt;code&gt;calculatePrice&lt;/code&gt; has surfaced a new concept: a &lt;code&gt;PricingPolicy&lt;/code&gt;. Not because a framework required it, not because a service boundary forced it, but because the model told you that pricing rules had become rich enough to deserve their own home. This is design evolution driven by the domain — the right kind of complexity, appearing at the right time, for the right reason.&lt;/p&gt;




&lt;h2&gt;
  
  
  The distributed workaround
&lt;/h2&gt;

&lt;p&gt;Teams that build procedural systems eventually hit the context problem at scale. Logic is spread across a growing codebase with no clear ownership. Rules diverge. The system becomes expensive to change. The industry's standard response is to enforce context through service boundaries. Order rules live in the Order service. Pricing rules live in the Pricing service. The boundary makes it structurally difficult for one service to reach into another's domain.&lt;/p&gt;

&lt;p&gt;This is attempting, through infrastructure, to solve a problem that a domain model solves through structure.&lt;/p&gt;

&lt;p&gt;The intuition is understandable. The result is a workaround that costs more than the problem it replaces.&lt;/p&gt;

&lt;p&gt;Consider what the VIP rush exemption requires in a distributed system. The Order service needs to price a rush order for a VIP customer. It cannot reach into the Pricing service's data — that violates the boundary. So it calls the Pricing service. But the Pricing service needs to know whether the customer is a VIP — and the Customer service owns that. Now the services are coupled in ways the original boundary was meant to prevent, or an orchestration layer is required to assemble inputs before calling either service, or an event-driven flow is constructed in which services react to each other asynchronously — introducing eventual consistency, message ordering concerns, and a debugging surface that spans multiple log streams.&lt;/p&gt;

&lt;p&gt;And this is before considering what happens when the action fails halfway through.&lt;/p&gt;

&lt;p&gt;In a monolith with a rich domain model, failure costs a database rollback. One word. The action either completed or it did not. There is no intermediate state. There is no question of what to clean up.&lt;/p&gt;

&lt;p&gt;In the distributed system, there is no transaction. If the order is created but the pricing service fails before responding, the system is in a partial state. That partial state must be resolved — not by the database, which knows nothing about it, but by compensating logic: a designed, implemented, tested, and maintained sequence of calls that undoes the steps that completed before the failure. For four services, the failure paths grow as O(n²). Each compensation is a domain operation that must be reachable, idempotent, and tested both in isolation and in combination.&lt;/p&gt;

&lt;p&gt;Before any of this business logic runs, the infrastructure required to support it exists permanently: a message broker, a saga framework or hand-rolled saga state table, distributed tracing with correlation IDs propagated through every service and every event envelope, an idempotency layer in every service because message brokers guarantee at-least-once delivery, API contracts and versioning because a breaking schema change is a production incident in every downstream service, and per-service CI/CD pipelines, databases, and operational overhead — multiplied by the number of services.&lt;/p&gt;

&lt;p&gt;None of this delivers business value. All of it exists solely to reconstruct, at permanent cost, the properties that a single database transaction provided for free: atomicity, consistency, rollback on failure, and a single coherent answer to what just happened.&lt;/p&gt;

&lt;p&gt;The VIP rush exemption — three sentences of business requirement — now requires coordinating across three services, with asynchronous event flows, compensating transactions, and a debugging surface that no single developer can hold in their head.&lt;/p&gt;

&lt;p&gt;The Russian space program used a pencil.&lt;/p&gt;




&lt;h2&gt;
  
  
  The refactorability that distribution destroys
&lt;/h2&gt;

&lt;p&gt;There is a cost of microservices that receives less attention than sagas and eventual consistency, but which compounds more severely over time: the loss of refactorability.&lt;/p&gt;

&lt;p&gt;In a rich domain model, a refactoring is a restructuring of code within a coherent boundary. If &lt;code&gt;PricingPolicy&lt;/code&gt; needs to become its own concept, the compiler identifies every place that needs to change. You make the changes, run the tests, deploy. The refactoring is complete.&lt;/p&gt;

&lt;p&gt;In a distributed system, a refactoring that touches a service contract is a migration. The event schema consumed by downstream services cannot simply change — it requires a versioning strategy, a migration window, a period of running old and new schemas simultaneously, and coordination across teams who own the downstream consumers. The boundary introduced to enforce ownership has become a fossilised contract. The ownership is preserved. The ability to evolve is not.&lt;/p&gt;

&lt;p&gt;This is the trade that distribution forces: you gain enforcement of service boundaries, and you lose the ability to change them cheaply. In a domain that is still being understood — which is most domains, for most of their lifetime — that trade is almost always wrong. The boundaries drawn at year one reflect year-one understanding. The domain will teach you things in year two that make those boundaries look naive. In a monolith with a rich domain model, you redraw the boundary and the compiler helps you. In a distributed system, you live with it, or you pay the migration cost. Most teams live with it. The boundaries fossilise. The system carries the imprint of how the domain was understood at its beginning, permanently.&lt;/p&gt;




&lt;h2&gt;
  
  
  When distribution is genuinely warranted
&lt;/h2&gt;

&lt;p&gt;Distribution has legitimate use cases. They share a common property: they are external constraints on the system, not assessments of the current domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proven, asymmetric load.&lt;/strong&gt; When one component has a demonstrably different scaling profile — proven by measurement under real conditions, not anticipated in theory — isolating it may be warranted. The question is not "could this theoretically need more scale?" It is "is this the measured bottleneck today, and does the cost of isolation exceed the cost of scaling the whole?" In most systems, no individual component is the bottleneck. The constraint is the atomic action as a whole. Scaling the whole is cheaper and simpler than the industry assumes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Physical or regulatory constraints.&lt;/strong&gt; When data must remain within a specific jurisdiction by law, geographic distribution is warranted. The right approach is to deploy a complete instance of the domain within that boundary — not to split the domain action across a jurisdictional boundary. The atomic action stays atomic. The domain model stays unified. What changes is the deployment target, not the architecture.&lt;/p&gt;

&lt;p&gt;Notice what is absent from this list: &lt;em&gt;domain concepts that currently appear independent.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Independence is a present-tense assessment of a future-tense system. Two concepts that have no transactional relationship today may acquire one tomorrow when a requirement arrives that neither anticipated. A recommendation engine and a payment processor appear independent until the business introduces a rule that links them. When that happens in a rich domain model, you answer a design question. When it happens in a distributed system, you face a migration — or you violate the boundary with a coupling that was supposed to be impossible, and accumulate the technical debt of a boundary that no longer reflects reality.&lt;/p&gt;

&lt;p&gt;Distribution should be warranted by constraints that are immune to domain evolution. Load and regulatory geography qualify. Current domain independence does not. It is a prediction dressed as a structural justification, and systems that are built on predictions about domain shape tend to look naive by the time they are old enough to evaluate.&lt;/p&gt;




&lt;h2&gt;
  
  
  The modelling capability problem
&lt;/h2&gt;

&lt;p&gt;A rich domain model does not build itself. It requires developers who can model — who can look at a domain, identify the concepts, understand their rules, and express those rules in objects that own them. This is a different skill from implementing features in a service layer. It is rarer, harder to teach, and not well served by the frameworks and patterns that dominate enterprise Java development.&lt;/p&gt;

&lt;p&gt;This is worth stating honestly, because it is the most common objection to everything argued above. "In theory, yes — in practice, we don't have the developers who can do this."&lt;/p&gt;

&lt;p&gt;The objection is real. But it is also a consequence of the same feedback loop. The industry has spent two decades building curricula, frameworks, and hiring pipelines around the service-DTO-repository pattern. Developers trained on Spring Boot are trained to think in services and data flows, not in domain concepts and object behaviour. The modelling skill atrophied because the dominant patterns did not require it — and then its absence became a justification for patterns that do not require it.&lt;/p&gt;

&lt;p&gt;The distributed architecture does not require modelling capability. It requires operational capability — the ability to manage brokers, sagas, contracts, and deployment pipelines. Those skills are available. They are well-documented. They are what the frameworks teach. So the distributed system gets built, not because it is the right architecture, but because it is the one the available skills support.&lt;/p&gt;

&lt;p&gt;What the industry normalised as "enterprise development" is, in significant part, the consequence of this skills gap and the infrastructure that grew up around it. The expensive architecture is the one that does not require the harder skill. The cheaper architecture — cheaper in every long-term dimension — requires developers who can model. Cultivating that capability is a different investment from buying more infrastructure. But it is the one with the compounding return.&lt;/p&gt;




&lt;h2&gt;
  
  
  But AI will fix this
&lt;/h2&gt;

&lt;p&gt;The most current version of the objection to everything argued above is not about developer skill. It is about AI coding tools. The argument runs: with AI assistance, the cost of writing procedural code drops dramatically. Features are generated in minutes. Boilerplate disappears. The velocity problem that made structural discipline seem expensive is solved by the tool. So the modelling skill gap does not matter — AI fills it.&lt;/p&gt;

&lt;p&gt;This is a plausible argument for small systems at early stages. It does not survive contact with the actual problem.&lt;/p&gt;

&lt;p&gt;AI coding tools are, in their current form, genuinely impressive at procedural implementation. Describe a feature clearly and the tool produces technically correct, well-structured code, fast. But the tool does not hold the domain. It holds the prompt. It implements what the prompt describes, in whatever pattern the surrounding codebase suggests — which in most enterprise codebases means a service method, a DTO, and a repository call. The implementation is correct with respect to the request. Whether it is consistent with the system's existing rules is a different question, and one the tool is structurally unable to answer reliably.&lt;/p&gt;

&lt;p&gt;The contradiction arrives quietly. In January, a developer prompts: "add a fifteen percent surcharge for rush orders." The AI implements it, correctly, in &lt;code&gt;PricingService&lt;/code&gt;. In March, a different developer prompts: "VIP customers should not pay extra for rush orders." The AI implements that too, correctly, somewhere in the call chain — perhaps in &lt;code&gt;OrderService&lt;/code&gt;, where the customer context is available. Both implementations are technically sound. Neither developer intended a contradiction. The AI had no way to know one existed, because the domain has no center. The rule "what does a rush order cost?" is not owned by anything. It is distributed across the history of prompts that touched it.&lt;/p&gt;

&lt;p&gt;In a rich domain model, this contradiction surfaces immediately. Both rules must live on &lt;code&gt;Order&lt;/code&gt;. When the second developer — or the AI they are directing — goes to implement the VIP exemption, the rush surcharge is already there, visible, in the same method. The conflict is structural and immediate. The developer makes a decision. The model is updated. The system reflects the current understanding of the business.&lt;/p&gt;

&lt;p&gt;In a procedural system, the conflict is invisible until a customer receives a price that is neither the intended standard price, nor the intended VIP price, but an artifact of two implementations that never knew about each other.&lt;/p&gt;

&lt;p&gt;There is a counterargument worth taking seriously: AI tools with sufficient codebase context — through large context windows, retrieval-augmented generation, or persistent memory across sessions — could theoretically detect such contradictions before implementing. Some tools already attempt this. The counterargument is real, and it would be wrong to dismiss it entirely.&lt;/p&gt;

&lt;p&gt;But even if the AI detects the contradiction, it cannot resolve it. The question "should VIP customers pay the rush surcharge?" is not answerable by reading the codebase. It is a business decision. The AI can surface the conflict. It cannot determine which rule reflects the current intent of the business, which rule is outdated, or whether both should coexist under different conditions. That requires domain understanding — and domain understanding requires a human with a model, not a tool with a context window.&lt;/p&gt;

&lt;p&gt;What the rich domain model provides is not a barrier to AI assistance. It is the structure that makes AI assistance most effective. When the domain is explicit, concepts are well-named, and rules are owned by the objects they govern, AI-generated code within that model tends to be good — because the model itself provides the context the AI needs to generate correctly. The right place to put a new rule is unambiguous. The existing rules are co-located and readable. The AI operates within a structure that guides it toward coherent output.&lt;/p&gt;

&lt;p&gt;The deeper issue is velocity. Procedural systems accumulate drift gradually, over years, as developers add logic wherever it is convenient. AI-assisted development does not change the direction of that drift. It changes the speed. What used to take three years of incremental addition now takes months of accelerated feature generation. The same structural absence of context ownership, at an order of magnitude higher throughput. The codebase grows faster than any team's ability to understand it, and the AI has no understanding to compensate with — only pattern matching against what is already there.&lt;/p&gt;

&lt;p&gt;AI does not fix the context problem. In a system without a domain model, it compounds it. The same rot, faster. The same contradictions, earlier. The same invisible price tag, arriving sooner.&lt;/p&gt;

&lt;p&gt;What AI changes is the cost of implementation. What it does not change — what nothing changes — is that implementation without structure is the most expensive kind. The structure has to come first. The model has to exist before the tool can be trusted to work within it. AI is a powerful accelerant. The question, as always, is what it is accelerating toward.&lt;/p&gt;




&lt;h2&gt;
  
  
  The invisible price tag
&lt;/h2&gt;

&lt;p&gt;Consider what a mature enterprise system built on microservices actually costs, outside the domain work itself.&lt;/p&gt;

&lt;p&gt;A containerised infrastructure running tens or hundreds of services. An orchestration layer — Kubernetes or equivalent — with its own operational model, upgrade cycle, and expertise requirement. A message broker cluster maintained for high availability. A distributed tracing stack. A log aggregation platform, because individual service logs are unreadable without one. A schema registry and contract testing infrastructure. Per-service CI/CD pipelines, each with its own configuration, deployment windows, and rollback strategy. An on-call rotation that covers distributed failure modes — partial outages, broker lag, compensation failures — that do not exist in a single-process system. A platform or infrastructure team whose entire function is to keep the operational substrate running.&lt;/p&gt;

&lt;p&gt;None of this is the domain. None of it delivers business value. All of it is the permanent operational cost of workarounds for missing context ownership.&lt;/p&gt;

&lt;p&gt;Now consider the same domain in a well-modelled monolith. A small number of deployable artefacts — perhaps one, perhaps a handful if genuine load asymmetry has been measured and justified. A relational database. A load balancer. Standard application monitoring. A CI/CD pipeline that deploys the whole. An on-call rotation that reads stack traces. The failure modes are the domain's failure modes, not the infrastructure's.&lt;/p&gt;

&lt;p&gt;The difference in team size, infrastructure cost, and operational overhead is not the cost of enterprise software. It is the cost of the workaround. The domain is the same. The business rules are the same. The problem being solved is the same. What differs is whether the system paid for a domain model or paid for the infrastructure required to simulate one.&lt;/p&gt;

&lt;p&gt;This difference is invisible in most organisations because the alternative was never built. The costs of the distributed system accumulate, get attributed to the scale and complexity of the enterprise domain, and become the benchmark against which new decisions are made. The next system is also built with microservices, because that is what enterprise software costs — and the incomparability between what was built and what could have been built means the attribution is never seriously questioned.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the rich domain model actually gives enterprise software
&lt;/h2&gt;

&lt;p&gt;The argument for the rich domain model in large enterprise systems is not that it is elegant or theoretically correct. It is that it is the mechanism by which enterprise software remains manageable over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Oversight.&lt;/strong&gt; When every rule about an order lives on &lt;code&gt;Order&lt;/code&gt;, a developer can understand order behaviour by reading one place. Not by reconstructing a distributed flow across services, event schemas, and asynchronous reactions. One place. This is not a convenience — it is what makes oversight possible as the system grows. Without it, understanding the system requires understanding its history, because the structure no longer maps to the domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Insight.&lt;/strong&gt; A rich domain model makes the domain legible to the team. The concepts are explicit. The rules are expressed in the language of the domain, not buried in service method conditionals and event handler logic. A new developer can read the model and understand the business. A non-technical stakeholder can, with modest translation, verify that the model reflects their understanding. That legibility is not incidental — it is the mechanism by which teams catch misunderstandings before they become bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simplicity under growth.&lt;/strong&gt; A procedural system grows by addition — new services, new methods, new conditions. A rich domain model grows by evolution — concepts become richer, responsibilities shift, new objects emerge when the design signals they are needed. Evolution is guided by the model. Addition is guided by expediency. Over five years, the difference in the resulting codebase is not marginal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preserved optionality.&lt;/strong&gt; A well-modelled domain in a single deployable can be split later, when measurement proves a specific boundary is warranted. The model already knows its own concepts — the split follows the domain's natural lines, guided by evidence. A distributed system cannot be reassembled cheaply once contracts have fossilised and team ownership has hardened around service lines. The simple starting point preserves optionality. The complex starting point spends it immediately, in exchange for flexibility that may never be needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  First principles
&lt;/h2&gt;

&lt;p&gt;There is nothing novel in the argument this article makes.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Structure your thinking before you structure your infrastructure.&lt;/em&gt; The question of where a rule lives is a question about the domain. Answer it in the domain — in the model, in the objects that own the concepts — before reaching for any infrastructure to enforce it. Infrastructure that enforces a boundary you have not yet thought through will enforce it permanently and expensively.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The location of a rule is part of the design.&lt;/em&gt; A rule in the right place is findable, testable, and changeable. A rule in the place that was convenient to add it becomes a historical artefact, discoverable only by reading the history of the system.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Complexity introduced to compensate for missing structure is the most expensive kind.&lt;/em&gt; It does not reduce over time. It compounds. Every saga that exists because a transaction boundary was removed, every contract that fossilises a year-one boundary decision, every service that owns zero domain concepts but exists to coordinate between services that do — these are permanent operational costs, paid every day, for the lifetime of the system.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What the industry calls the cost of enterprise software is largely the cost of not modelling.&lt;/em&gt; The infrastructure, the teams, the operational overhead — these are not the price of scale or complexity. They are the price of workarounds for a missing domain model, normalised by the fact that everyone around you is paying the same price and the alternative was never built to compare against.&lt;/p&gt;

&lt;p&gt;The rich domain model is not a technique for senior engineers on greenfield systems. It is the thing that makes enterprise software manageable at all — the only mechanism that preserves oversight, insight, and simplicity as a system grows. The alternative is the same complexity, without the structure to contain it, with an expensive distributed scaffolding erected around it to simulate the containment the model would have provided for free.&lt;/p&gt;

&lt;p&gt;Build the model. Let the model tell you where the rules live, when the design needs to evolve, and when — if measurement ever demands it — a boundary has genuinely earned the right to become a service.&lt;/p&gt;

&lt;p&gt;The model will not mislead you. The path of least resistance will.&lt;/p&gt;

</description>
      <category>softwaredevelopment</category>
      <category>microservices</category>
      <category>distributedsystems</category>
      <category>oop</category>
    </item>
    <item>
      <title>Engineering a UI for a Java Backend: Maintainability, Longevity, and Why the Answer Might Surprise You</title>
      <dc:creator>Leon Pennings</dc:creator>
      <pubDate>Wed, 13 May 2026 07:59:01 +0000</pubDate>
      <link>https://dev.to/leonpennings/engineering-a-ui-for-a-java-backend-maintainability-longevity-and-why-the-answer-might-surprise-3m7p</link>
      <guid>https://dev.to/leonpennings/engineering-a-ui-for-a-java-backend-maintainability-longevity-and-why-the-answer-might-surprise-3m7p</guid>
      <description>&lt;p&gt;Most teams pick a UI framework the same way they pick a restaurant — by what is popular right now, what colleagues recommend, or what appeared at the top of a search result. This article takes a different approach: establish what a well-engineered UI for a Java backend actually needs to be, from first principles, and then see what framework honestly satisfies those requirements. The conclusion may not be what you expect.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: Where the Client Lives
&lt;/h2&gt;

&lt;p&gt;Before requirements, one distinction that frames everything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server-side rendering:&lt;/strong&gt; the client lives on the server. The server maintains state, computes views, and pushes HTML to the browser. The browser is a display terminal. Every interaction is a round-trip. Network interruptions break the experience. Horizontal scaling requires session affinity or replication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fat client:&lt;/strong&gt; the client lives in the browser. It holds its own state, manages its own behaviour, and calls the server only when it needs data or needs to record an action. Server calls are as simple as API calls. The server is stateless. Network interruptions are survivable. Any server instance handles any request.&lt;/p&gt;

&lt;p&gt;This distinction is not a stylistic preference. It determines where state lives, how the system scales, how resilient the user experience is to infrastructure events, and what the server is actually responsible for. Everything that follows builds on it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: The Requirements
&lt;/h2&gt;

&lt;p&gt;These requirements are not Java-specific preferences. They describe what any disciplined engineering team should want from a UI layer, regardless of backend language. Java is the context. The principles are universal.&lt;/p&gt;

&lt;p&gt;The architecture has three distinct layers, each with different skill requirements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Skills Required&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Platform / component&lt;/td&gt;
&lt;td&gt;Defines HTML structure, CSS, GWT wrappers&lt;/td&gt;
&lt;td&gt;Semantic HTML, CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Communication infrastructure&lt;/td&gt;
&lt;td&gt;Communication between browser and server&lt;/td&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feature development&lt;/td&gt;
&lt;td&gt;Views, interactions, domain behaviour&lt;/td&gt;
&lt;td&gt;Java only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The requirements below apply to the architecture as a whole. The skill boundary is explicit: HTML and CSS expertise is required at the component layer, and only there. Feature developers — the majority of the team, doing the majority of the work — operate entirely in Java.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Frontend-Requirement-Down Design
&lt;/h3&gt;

&lt;p&gt;The UI should be designed from what the user needs to accomplish, not from what the backend domain model happens to look like. User interactions frequently span multiple backend domain objects. Designing upward from DTOs or entity shapes produces interfaces that reflect implementation details rather than user intent. The frontend is a peer application with its own concerns — not a projection of the server model.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Browser is the Client's Home
&lt;/h3&gt;

&lt;p&gt;The client must live in the browser. A fat client holds its own state, survives server restarts and transient network interruptions, and communicates with the server only when necessary. Client-side state is typed, structured, and available across the full session — without cookies, without server-side session objects, without distributed session infrastructure. The server is stateless. Scaling follows directly. This is not a performance preference — it is an architectural correctness preference with operational consequences that compound over the lifetime of the system.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Compile-Time Validation over Runtime Discovery
&lt;/h3&gt;

&lt;p&gt;Structural integration errors — type mismatches, missing handler implementations, incorrect data shapes, gaps between UI and backend contracts — should fail at build time rather than in browser execution. If the Maven build passes, the integration is correct. Treating the browser as the place where structural errors are discovered is an avoidable cost in debugging time, deployment cycles, and user impact.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Minimal Boilerplate per Feature
&lt;/h3&gt;

&lt;p&gt;Adding a new feature — a new view, a new action, a new data field — should require changes in the minimum number of places, ideally one. The codebase structure should guide the developer to the correct location and pattern. Architectural decisions should not be reopened on every addition.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. 100% Ownership of Components — No Escape Hatches
&lt;/h3&gt;

&lt;p&gt;Component frameworks typically define generic components covering the majority of use cases, then offer escape hatches for the rest. This is presented as flexibility. In practice it is a structural liability: escape hatches couple the project to framework internals, and framework upgrade cycles risk breaking those couplings. Long-term maintainability improves substantially when the project owns its rendered HTML and component contracts completely, so the question of escaping the framework never arises.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Semantic HTML is Non-Negotiable — and Must Be Owned
&lt;/h3&gt;

&lt;p&gt;The browser is a world of HTML. Producing semantically correct, standards-compliant HTML should be an explicit engineering goal — not an afterthought, not something delegated to a third-party framework's component library.&lt;/p&gt;

&lt;p&gt;Adopting a framework's component library because "we are not HTML/CSS experts" trades a knowledge gap for a control gap. The framework's HTML is a black box. When it changes its DOM structure, CSS breaks. When it revises class naming conventions, the project adapts. The project is permanently downstream of someone else's HTML decisions, on someone else's release cycle.&lt;/p&gt;

&lt;p&gt;The correct response is to own the HTML. The investment is made once: define each component in clean, semantically correct HTML. The resulting HTML belongs to the project. It cannot be broken by a third-party upgrade. The component HTML should remain clear and concise — obvious to anyone who opens the file — so that maintenance is equally obvious.&lt;/p&gt;

&lt;p&gt;The CSS for each component lives in the same file used to define the component's HTML. One source of truth. No indirection. No ambiguity about which styles apply to which structure. When a component needs to change, HTML and CSS are reviewed together. One CSS file styles the entire application. Component class names are functional and identifiable — they reflect what the component &lt;em&gt;is&lt;/em&gt;, not what it looks like. CSS can evolve entirely independently of Java code. A designer can restyle the full application by modifying CSS alone, without touching a single Java class.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Any Java Developer Can Build Application Features — No JavaScript Ecosystem Expertise Required
&lt;/h3&gt;

&lt;p&gt;Any Java developer should be able to build application features within this UI architecture without requiring JavaScript, CSS, or HTML knowledge. Not a full-stack developer. Not a Java developer who also knows a JS framework. Any Java developer.&lt;/p&gt;

&lt;p&gt;The developer base for Java is large. The developer base for Java developers who are also proficient in modern JavaScript, CSS architecture, and semantic HTML is substantially smaller. A framework requiring that intersection creates a staffing constraint that compounds as the team changes over time.&lt;/p&gt;

&lt;p&gt;UI engineering involves more than syntax — interaction design, state modelling, async behaviour, information hierarchy. These remain the developer's responsibility. What this architecture removes is the requirement to acquire a second language ecosystem to express them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: Why These Are the Right Requirements
&lt;/h2&gt;

&lt;p&gt;Each requirement is independently justifiable. Together they reinforce each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend-requirement-down&lt;/strong&gt; is product thinking applied to architecture. The backend serves the frontend; the frontend serves the user. Reversing this dependency produces interfaces that feel like database forms — and that break whenever the domain model evolves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fat client as the client's home&lt;/strong&gt; reflects what the browser is: a capable, stable application runtime. Treating it as a display terminal forces server infrastructure to compensate for what the client could handle locally — state management, session continuity, resilience to transient failures. These become server problems when they could be client responsibilities, solved more cheaply, closer to the user, without cross-request infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compile-time validation&lt;/strong&gt; is the highest-leverage quality tool available to the team. Every structural error that escapes the build and reaches the browser costs more to find and fix by a significant margin. The compiler is free at runtime. Moving validation earlier is always the better trade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;100% component ownership&lt;/strong&gt; is the only durable resolution to the escape hatch problem. Partial ownership — using a framework's components for most cases — means living with the framework's HTML decisions, its upgrade cycle, and its constraints indefinitely. Full ownership means none of that. The project defines the components. The project owns the HTML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Owning semantic HTML&lt;/strong&gt; is not idealism — it is engineering discipline. HTML is the foundation of everything the browser renders. Teams that do not own their HTML foundation do not fully control their accessibility, CSS architecture, DOM structure, or maintenance costs. A shared component library means this investment is made once and leveraged across every application in the organisation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Large accessible developer base&lt;/strong&gt; recognises that sustainable software is built by teams over time. An architecture requiring rare skill intersections is a staffing risk. Reducing the entry requirement for feature development to "knows Java" is a durable organisational advantage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4: Why Popular Alternatives Fall Short
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;React / TS&lt;/th&gt;
&lt;th&gt;Thymeleaf / HTMX&lt;/th&gt;
&lt;th&gt;Vaadin (Flow)&lt;/th&gt;
&lt;th&gt;Required from the architecture&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compile-time structural validation&lt;/td&gt;
&lt;td&gt;Partial — no unified cross-language bridge&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client lives in the browser&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single language — Java&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100% component ownership&lt;/td&gt;
&lt;td&gt;Possible, rarely achieved&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;By construction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No JS ecosystem expertise needed&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stateless server&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No (standard architecture)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML ownership by construction&lt;/td&gt;
&lt;td&gt;Possible, not guaranteed&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  JavaScript Frameworks (React, Angular, Vue, Svelte)
&lt;/h3&gt;

&lt;p&gt;These frameworks can build any UI a browser can render. The evaluation here is not about output capability — it is about architectural fit for a Java backend team.&lt;/p&gt;

&lt;p&gt;A Java team adopting a JavaScript framework acquires a second language, a second type system, a second build toolchain, and a second ecosystem to maintain in parallel with the Java backend. The type systems do not share a validation boundary: TypeScript validates the client, Java validates the server, and structural mismatches between them surface at runtime. Shared contracts must be maintained in two places by two compilers.&lt;/p&gt;

&lt;p&gt;Component ownership is theoretically possible in these frameworks but structurally not guaranteed. A disciplined team can own their HTML in React. Most teams, in practice, adopt component ecosystems that control the DOM on their behalf — trading ownership for convenience and inheriting the maintenance consequences. The architecture described in this article makes full HTML ownership the default, not the exception.&lt;/p&gt;

&lt;p&gt;Framework churn is a real cost. The JavaScript ecosystem changes significantly on a multi-year cycle. Architectural commitments made today carry implicit future migration costs. These are difficult to quantify at decision time and easy to underestimate.&lt;/p&gt;

&lt;p&gt;For a Java backend team building domain applications, the trade-offs do not stack up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server-Side Rendering (Thymeleaf, Spring MVC, JSP, HTMX)
&lt;/h3&gt;

&lt;p&gt;The client lives on the server. Every interaction is a round-trip. State requires server-side session management. Horizontal scaling requires session affinity or replication. Template expressions are strings — a renamed Java method leaves a broken template the build cannot detect. Dynamic behaviour requires JavaScript added on top, reintroducing a dependency without the benefits of a proper fat client.&lt;/p&gt;

&lt;p&gt;Reasonable choices for content sites and simple form-based applications. Not suited for the class of domain application this article addresses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vaadin
&lt;/h3&gt;

&lt;p&gt;Targets the same use case as this architecture. In its standard configuration, recent Vaadin versions run server-side UI logic over a persistent WebSocket connection, which reintroduces server state and makes server restarts visible to users. Every UI interaction crosses the network. The fat client advantage — local state, local computation, resilience — is surrendered. Earlier Vaadin versions used GWT as their client-side foundation, which is architecturally much closer to what this article describes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5: What the Architecture Needs — and What Delivers It
&lt;/h2&gt;

&lt;p&gt;The requirements converge on a specific model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A &lt;strong&gt;Java application&lt;/strong&gt; that runs in the browser&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Compiled by a &lt;strong&gt;Java toolchain&lt;/strong&gt; into a browser-executable artifact&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;With &lt;strong&gt;full ownership of HTML output&lt;/strong&gt; through a project-defined component library&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Communicating with the Java backend through a &lt;strong&gt;typed, compiler-validated protocol&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Built and verified by a &lt;strong&gt;single Maven build&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not a description of a framework. It is a description of a &lt;strong&gt;compiler that targets the browser runtime&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The mental model is exact:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Java → JVM bytecode → runs on Linux, Windows or any JVM host&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Java → browser-executable artifact → runs in any browser&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The browser is a runtime environment, just as the JVM is a runtime environment. The developer writes Java. The compiler bridges the language to the runtime. The output format — bytecode or JavaScript — is an implementation detail, not a concern of the developer. This repositions browser compilation from exotic to normal. It is the same problem Java solved for heterogeneous server environments, applied to a new runtime target.&lt;/p&gt;

&lt;p&gt;Once this model is understood, the selection question becomes concrete: what exists that actually delivers it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GWT — the Google Web Toolkit — is the only mature, production-proven option for Java.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GWT compiles Java to JavaScript. It has done so since 2006, at Google scale. Its type system is Java's type system. Its build integration is Maven. Its module boundaries are compiler constraints — client-only code cannot be invoked on the server; server-only code is not compiled into the client artifact.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Critical Distinction: GWT as Compiler vs GWT as Component Framework
&lt;/h3&gt;

&lt;p&gt;This distinction is architectural, not rhetorical.&lt;/p&gt;

&lt;p&gt;GWT used with JSON communication and its built-in widget library is, in practice, just another web framework. The compiler provides Java syntax, but the architecture is conventional: a third-party component library controls the HTML, communication is untyped, and the project is downstream of GWT's component ecosystem. In this mode GWT offers limited advantage and inherits familiar maintenance liabilities.&lt;/p&gt;

&lt;p&gt;GWT used as a pure compiler — with a project-owned component library and a typed communication protocol — is a fundamentally different thing. The HTML belongs to the project. The CSS belongs to the project. The communication contracts are validated by the Java compiler. GWT provides one thing: the ability to write Java that runs in the browser. Everything else is owned by the project.&lt;/p&gt;

&lt;p&gt;This distinction also resolves the "GWT is dead" criticism at a structural level. If GWT's component ecosystem were abandoned tomorrow, a project using GWT as a pure compiler would be unaffected. The compiler is the only dependency — and compilers are among the most stable software artifacts in existence. Stability is not abandonment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 6: The Component Library — HTML Owned, CSS Independent, Java Exposed
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Semantic HTML Defined Once, Owned Completely
&lt;/h3&gt;

&lt;p&gt;HTML and CSS specialists define every component from scratch. This is a one-time investment — made properly, by people with the relevant expertise — that benefits every subsequent line of feature code written against it. The library covers the full UI vocabulary: root layout, navigation, header, footer, main content area, tables, lists, description lists, forms, dialogs, buttons, selects, confirmation prompts, composite panels.&lt;/p&gt;

&lt;p&gt;Each component is clean, semantic HTML. The structure of a table is &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; with &lt;code&gt;&amp;lt;thead&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;tbody&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;th&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;td&amp;gt;&lt;/code&gt;. Navigation is &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt;. A description list is &lt;code&gt;&amp;lt;dl&amp;gt;&lt;/code&gt; with &lt;code&gt;&amp;lt;dt&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;dd&amp;gt;&lt;/code&gt;. The HTML communicates intent to the browser, to assistive technologies, and to any developer who opens the file. It should remain clear and concise — maintenance should be obvious from inspection.&lt;/p&gt;

&lt;p&gt;The CSS for each component lives alongside its HTML definition. One source of truth. A developer maintaining a component sees structure and styling together. One CSS file styles the entire application. Class names are functional — &lt;code&gt;transfer-table&lt;/code&gt;, not &lt;code&gt;blue-bordered-grid&lt;/code&gt;. Visual redesign is a CSS concern. It requires no Java changes and no recompile of application code.&lt;/p&gt;

&lt;h3&gt;
  
  
  GWT Wraps Each Component in a Typed Java Class
&lt;/h3&gt;

&lt;p&gt;For each HTML component definition, a GWT Java class emits the correct HTML structure and exposes a typed Java API: builder methods, typed parameters, event handlers — all in Java, all compiler-validated.&lt;/p&gt;

&lt;p&gt;Building a wrapper requires knowing what an HTML tag is and when to use it — not CSS, not JavaScript, not layout theory. GWT provides the primitives: set a tag name, compose child elements, assign a class attribute. The wrapper author works in Java, guided by the HTML definition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Above the wrapper layer, feature developers never write HTML. They never reference a CSS class name. They never open a stylesheet.&lt;/strong&gt; They instantiate typed Java classes. The HTML is an implementation detail of the wrapper. The CSS is an implementation detail of the stylesheet. Neither is visible, relevant, or accessible to feature developers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared Libraries Across the Organisation
&lt;/h3&gt;

&lt;p&gt;The component library is a standalone Maven artifact. Multiple applications can depend on it. Every application in the portfolio gets uniform, semantically correct, standards-compliant HTML automatically. Each application maintains its own CSS where visual design differs — or shares it where it does not.&lt;/p&gt;

&lt;p&gt;HTML standards adherence is guaranteed across the portfolio by one maintained library, not by discipline in each project. Accessibility improvements propagate from one place to every application on the next build. When semantic best practices evolve, one wrapper update benefits all consumers.&lt;/p&gt;

&lt;p&gt;A strict separation is enforced by construction: component styling lives in the library, screen-level code lives in the application. A developer building a screen cannot accidentally mix component-level styling concerns into application code because they never touch CSS at all. This is the kind of separation that is hard to achieve through convention and trivial to achieve through architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Maintenance Cycle Reframed
&lt;/h3&gt;

&lt;p&gt;Conventional framework maintenance involves upgrading versions, adapting to API deprecations, resolving conflicts between framework changes and application code, and re-learning patterns the framework revised.&lt;/p&gt;

&lt;p&gt;In this architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Visual redesign:&lt;/strong&gt; update the CSS file. No Java touched. No application recompile.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;HTML structure change:&lt;/strong&gt; update one wrapper class. Application code above it is untouched.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;New component:&lt;/strong&gt; define the HTML, write the wrapper. Immediately available to all feature developers as a typed Java class.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GWT compiler update:&lt;/strong&gt; affects only the compiler, not the component API or application code.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dependency on GWT is a dependency on a compiler. Compiler interfaces are more stable than component framework APIs. The upgrade cost is proportional to what actually changed — not to what the framework decided to revise.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 7: The Communication Architecture — One Pattern, Always
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Command as the Unit of Interaction
&lt;/h3&gt;

&lt;p&gt;Every client-server interaction is a Command — a Java object in the GWT shared package, serializable over GWT-RPC, carrying both the request parameters and, on return, the result. A single RPC endpoint receives all Commands and routes them to Visitor handlers. No servlet proliferation. No REST design decisions. No JSON schema. No API documentation to keep synchronised with implementation.&lt;/p&gt;

&lt;p&gt;A Command carries its request parameters to the server. The Visitor populates the result on the same Command object. The Command returns to the client. The same type throughout. The compiler validates the entire round trip.&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;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GetIntendedTransferDetailsCommand&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;IntendedTransferDetails&lt;/span&gt; &lt;span class="n"&gt;intendedTransferDetails&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;//for serializable purposes&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;GetIntendedTransferDetailsCommand&lt;/span&gt;&lt;span class="o"&gt;(){}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;GetIntendedTransferDetailsCommand&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id&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="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setIntendedTransferDetails&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IntendedTransferDetails&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;intendedTransferDetails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;details&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;IntendedTransferDetails&lt;/span&gt; &lt;span class="nf"&gt;getIntendedTransferDetails&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;intendedTransferDetails&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;Used on the client:&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GetIntendedTransferDetailsCommand&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transferId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CommandResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GetIntendedTransferDetailsCommand&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;@Override&lt;/span&gt;
        &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onResult&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GetIntendedTransferDetailsCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;            &lt;span class="n"&gt;panel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getIntendedTransferDetails&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getWidget&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;reload&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;Any Java developer reads this and understands it immediately. Create a Command with parameters. Execute it asynchronously. The result arrives back on the same Command object. Call whatever you need. No HTTP verbs. No JSON mapping. No async framework to learn. Java objects, Java callbacks, Java types throughout.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Server-Side Lifecycle
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Command arrives at single RPC endpoint
  → Extract session UUID from Command
  → Load and validate UserSession from database
  → Set user context in ThreadLocal       (Interaction begins / transaction opens)
  → Route to Visitor handler by Command type
  → Visitor executes domain logic
  → Visitor populates result on Command
  → Command returned to client
  → ThreadLocal cleared                   (Interaction ends / transaction closes)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Interaction scope is the transaction scope. Every Command is exactly one Interaction, one transaction boundary, one security check. This is structural — it cannot be accidentally skipped. Server-side input validation applies as in any server-side system; the Interaction boundary enforces scope, not content correctness.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security as a Type Property
&lt;/h3&gt;

&lt;p&gt;Commands requiring elevated privileges implement marker interfaces — &lt;code&gt;RequiresAdministrator&lt;/code&gt;, for example. The infrastructure checks for these before routing to any application code. Security is declarative, compile-time visible, and cannot be bypassed from application code. No annotation processing, no AOP, no filter chain configuration. Java interfaces and a single infrastructure check. Every privileged Command in the codebase is identifiable by a type search.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding a Feature
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Add a Command class in the shared package&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add a Visitor implementation on the server&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Call the Command from the client&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No servlet registration. No routing configuration. No JSON schema. No API documentation update. The type system connects Command to handler. The compiler verifies the connection. Maven validates the whole.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 8: Object-Oriented UI — A Natural Consequence
&lt;/h2&gt;

&lt;p&gt;GWT enables a pattern that most frontend architectures make difficult or impossible: applying standard object-oriented principles directly to UI objects. This is not a requirement of the architecture — it is a possibility it unlocks, and one worth examining because it illustrates how far the "just Java" principle extends when taken seriously.&lt;/p&gt;

&lt;p&gt;In most frontend architectures, data and behaviour are separated by design. A data object carries fields. Separate components, controllers, reducers, or stores manage what happens when the user interacts with that data. The data object is inert — it knows nothing of its own presentation or behaviour.&lt;/p&gt;

&lt;p&gt;In a Java fat client, this separation is a choice, not a constraint. A domain summary object can carry both its data and its behaviour, exactly as a well-designed Java object does in any other context.&lt;/p&gt;

&lt;p&gt;The same class that defines how a &lt;code&gt;TransferSummary&lt;/code&gt; appears in a table also defines what happens when the user clicks a row: which popup appears, which actions are offered, which Commands are issued for each action, which dialogs are composed for data entry. All co-located. All in Java. The object is alive in browser memory — it holds its full operational context, not just its display data.&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;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OnResult&lt;/span&gt; &lt;span class="n"&gt;onResult&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Popup&lt;/span&gt; &lt;span class="n"&gt;popup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Popup&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getSummarizedSummary&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;popup&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addPopupButton&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"View details"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;showTransferDetails&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;popup&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addPopupButton&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Edit transaction"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GetEditableTransferDetailsCommand&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sourcePersonId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serviceProviderId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTransferDetails&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;edit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onResult&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;popup&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;show&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;strong&gt;Adding a field means editing one class.&lt;/strong&gt; Add the field. Add the table header column. Add the table row cell. One place, one commit, compiler-validated. Compare this to a conventional layered approach: add to backend DTO, update TypeScript interface, update table component, update API response mapper, update state store, update tests per layer. Multiple locations, across potential team boundaries, where a mismatch at any point is a runtime surprise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conditional UI behaviour is conditional Java.&lt;/strong&gt; Whether to show a "Remove agreement" button is &lt;code&gt;if (agreements.size() &amp;gt; 0)&lt;/code&gt; — a domain condition expressed directly, not a separate UI state flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No DTO duplication.&lt;/strong&gt; There is no parallel UI model that mirrors the domain object. The domain object &lt;em&gt;is&lt;/em&gt; the UI object. The object carried to the client is the object that renders, the object that acts, the object that issues Commands. One model, one place, no synchronisation required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Summary objects carry more than they display.&lt;/strong&gt; A summary may show five fields in a table but carry twelve. The hidden fields are operational context — entity identifiers, related references, state flags — that drive popup actions and Command parameters. This is only possible because the fat client keeps the full object in browser memory. No round-trip to reconstruct context. No hidden state pushed into the URL.&lt;/p&gt;

&lt;p&gt;This is basic encapsulation applied consistently. It is not a novel pattern. It is OO design working exactly as intended, in a context where most architectures actively prevent it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 9: Testing in a Compiler-Validated Architecture
&lt;/h2&gt;

&lt;p&gt;Compiler validation eliminates a specific class of error: structural integration failures. Type mismatches, missing Visitor implementations, incorrect method signatures, RPC serialization failures — these do not reach runtime in a correctly built system.&lt;/p&gt;

&lt;p&gt;This does not replace testing. It removes the need for a category of test.&lt;/p&gt;

&lt;p&gt;Domain logic, workflow correctness, UX behaviour, edge cases in user interactions, and business rule validation all require tests. The compiler is not a substitute for verifying that the system does the right thing — it is a guarantee that the system does not break structurally. These are different concerns and both matter.&lt;/p&gt;

&lt;p&gt;In practice this means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Unit tests&lt;/strong&gt; cover domain logic and Visitor behaviour — pure Java, fast, no browser required. A unittest that checks that all Commands have 1 corresponding Visitor implementation ensures all commands can be executed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Integration tests&lt;/strong&gt; cover workflow correctness and Command/Visitor round trips&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The compiler covers&lt;/strong&gt; structural integration: type contracts, module boundaries, serialization correctness&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a test suite that is smaller, faster, and more focused than one that must also catch structural integration failures at runtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 10: Simplicity as an Economic Argument
&lt;/h2&gt;

&lt;p&gt;The requirements above are engineering arguments. They have a direct economic translation.&lt;/p&gt;

&lt;p&gt;Getting something to work is the scope for prototypes. Building so that maintainability and cost are optimal is the scope for production code. Almost any framework clears the first bar. Very few clear the second consistently over time. The economic argument for this architecture is entirely a production-scope argument.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation speed.&lt;/strong&gt; A feature developer adds a panel by writing a Command, a Visitor, and composing typed Java components. No context switch, no second toolchain, no JSON mapping, no parallel type maintenance. The pattern is always the same. A developer who has built one feature understands the pattern for all subsequent features. Onboarding is measured in hours, not weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance cost.&lt;/strong&gt; The dominant long-term cost in software is not building features — it is maintaining them. In this architecture, maintenance is localised by construction. A field change is one class. A visual redesign is CSS. An HTML structure update is one wrapper. There is no architectural archaeology to determine where a change belongs. Changes do not ripple.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Upgrade cycle cost.&lt;/strong&gt; Framework upgrade cost in JavaScript-heavy projects is a recurring drain on development capacity. Major upgrades require rework proportional to how deeply the framework is woven into the application. In this architecture the upgrade cycle is: update CSS when design standards evolve, update wrapper classes when HTML best practices change, update the GWT compiler when a new version is available. Application code is untouched by all three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payload and runtime.&lt;/strong&gt; A mature production application with 3,000–4,000 UI classes produces 15–25MB of compiled, obfuscated output. This figure covers all application logic, all UI behaviour, and all dynamically generated HTML — the base HTML page is a minimal shell with an empty body; everything visible is generated by the compiled client. On modern connection speeds, for domain applications used by authenticated users, this is paid once and cached. It is not a meaningful operational concern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Organisational scale.&lt;/strong&gt; A shared component library amortises the HTML investment across every application in the portfolio. Accessibility improvements, semantic updates, and visual redesigns propagate from one place. Teams across multiple projects work from the same HTML foundation without coordinating on it. The per-application maintenance cost trends toward the cost of application logic alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team composition.&lt;/strong&gt; Because any Java developer can build UI features, the team does not maintain a specialist frontend/backend split with the communication overhead that implies. Junior developers contribute from day one. Senior developers are not bottlenecked on UI concerns. The team required to maintain and extend the system is smaller and easier to staff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Future safety.&lt;/strong&gt; HTML and CSS have been backward-compatible for thirty years. An architecture founded on semantic HTML and a Java compiler is not a bet on a framework's commercial continuity — it is a bet on the web platform itself. The compiler-plus-owned-library combination means no part of the stack is dependent on a third party making the right product decisions.&lt;/p&gt;

&lt;p&gt;Correctness and economy point in the same direction. That is a consequence of building from first principles rather than from accumulated convention.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 11: Addressing the Criticisms
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "GWT is abandoned / dead"
&lt;/h3&gt;

&lt;p&gt;GWT 2.13.0 was released February 11, 2026. GWT 2.12.2 in March 2025. 2.12.1 in November 2024. 2.12.0 in October 2024. This is an active project with a consistent release cadence. Version 2.13 removed legacy IE polyfills, modernised project samples to Maven multi-module structure, added JFR events for compiler observability, delivered the largest JRE emulation improvements since 2.9.0, and added support for Jakarta Servlet APIs — meaning this stack runs cleanly on Spring Boot 3 and modern Jakarta EE servers.&lt;/p&gt;

&lt;p&gt;Structurally: GWT's JRE emulation has been progressively migrated to JsInterop to converge with J2CL, Google's next-generation Java-to-browser compiler. The API surface — Elemental2, JsInterop annotations, jsinterop-base — is shared between them. J2CL is Google's internal compiler for production-scale web applications today; GWT remains the most stable, Maven-integrated distribution for enterprise teams building on this model.&lt;/p&gt;

&lt;p&gt;More fundamentally: this architecture depends on GWT as a compiler, not as a component framework. The project-owned HTML, CSS, and communication infrastructure are independent of GWT's component ecosystem entirely. The compiler is the only dependency — and compilers are among the most stable artifacts in software. The Java compiler's core behaviour has not changed in years. Stability is not abandonment.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Compile times are too long"
&lt;/h3&gt;

&lt;p&gt;In a mature production application with 3,000–4,000 UI classes, compile times on modern hardware run between one and two minutes. Compile time is a function of permutation count — one permutation per browser per locale combination. With a single RPC endpoint, no code splitting, and a controlled locale set, permutation count is minimal. GWT 2.13.0 added JFR events specifically for compiler observability, making it straightforward to profile and address any compile-time concern. This criticism has most force for large multi-permutation applications. It does not apply here.&lt;/p&gt;

&lt;h3&gt;
  
  
  "The widget library is inadequate"
&lt;/h3&gt;

&lt;p&gt;Correct — and beside the point. This architecture does not use GWT's widget library. The HTML/CSS component library is defined by the project and owned by the project. The quality of GWT's built-in widgets is irrelevant.&lt;/p&gt;

&lt;h3&gt;
  
  
  "You still need to know HTML"
&lt;/h3&gt;

&lt;p&gt;At the component wrapper layer, the author needs to know what an HTML tag is and when to use it. Above that layer, feature developers work entirely in Java. The HTML knowledge required is modest, applied once per new component type, and confined to the component library team. It is explicitly not a feature development concern.&lt;/p&gt;

&lt;h3&gt;
  
  
  "There is no defined development approach in GWT"
&lt;/h3&gt;

&lt;p&gt;This article defines one. Command/Visitor for all communication. Project-owned component wrappers for all HTML output. Domain objects carrying their own UI behaviour. Maven as the single build and validation step. The criticism applies to teams using GWT without architectural intent. The architecture is the answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 12: When This Architecture Is Not the Right Choice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Public-facing, SEO-critical sites&lt;/strong&gt; are a different problem domain. This architecture delivers a minimal HTML shell and populates the body dynamically. For tools used by people to get work done, that is the right trade-off. For sites whose success depends on search engine indexing of content or on first-render performance for anonymous users, use server-side rendering. These are different problems and should be solved with different tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teams without Java as their primary language&lt;/strong&gt; will find less leverage here. The architecture's value comes from keeping Java developers in Java. A team already fluent in TypeScript and React is not gaining that advantage — they would be acquiring a new tool rather than deepening an existing strength.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deep JavaScript ecosystem integration&lt;/strong&gt; — complex native browser API interop, third-party JavaScript widgets with no Java wrapper, WebGL pipelines — may add friction at the GWT boundary. GWT provides JsInterop for these scenarios, but it requires the component layer author to understand that boundary explicitly.&lt;/p&gt;

&lt;p&gt;Stating these boundaries is scope clarity, not concession. This architecture is designed for domain applications: business tools, dashboards, admin interfaces, internal platforms, data-intensive workflows. For that class of application, it is the strongest available option.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Evaluated from first principles — not by popularity, not by ecosystem size, not by recency — the architecture described here is the most coherent, most maintainable, and most economically sound approach to UI development for Java backend domain applications.&lt;/p&gt;

&lt;p&gt;The key insight is not about GWT specifically. It is about where the client should live, what the build should guarantee, and what the team should need to know. The client lives in the browser — fully, not as a thin view over server state. The build guarantees structural correctness — by construction, not by convention. Feature developers work in Java — without HTML, CSS, or JavaScript knowledge, as a daily reality, not an aspiration.&lt;/p&gt;

&lt;p&gt;GWT, used as a compiler combined with a project-owned component library, is the only mature option that delivers this model. The compiler provides the Java-to-browser bridge. The component library provides the HTML ownership. Together they provide something no JavaScript framework and no server-side rendering approach offers in this combination: a complete, type-safe, Java-only feature development experience for domain application UI, where the web platform is the foundation and no third party controls the HTML.&lt;/p&gt;

&lt;p&gt;The criticisms dissolve on contact with the architecture: compile times are fast at the permutation counts this setup requires; the widget library is irrelevant because it is not used; abandonment misreads compiler stability as stagnation; and the HTML boundary is exactly as thin as it needs to be — confined to the component layer, invisible above it.&lt;/p&gt;

&lt;p&gt;The result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Any Java developer builds UI features from day one&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Maven build is the integration test&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Adding a feature is one Command, one Visitor — the same pattern, always, guided by the type system&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Visual evolution is CSS. HTML evolution is a wrapper update. Application code is untouched by both.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The client lives in the browser. The server is stateless. Scaling follows directly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Component ownership is total. The HTML is yours. The CSS is yours. No escape hatches, because there is nothing to escape from.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Implementation is faster. Maintenance is cheaper. Teams stay smaller. Upgrade costs are minimal. The foundation is the web platform itself.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no simpler, no more maintainable, no more economically defensible UI architecture for Java backend domain applications — when GWT is understood for what it is: a compiler that makes the browser a first-class Java runtime target, combined with the discipline to own everything above it.&lt;/p&gt;

</description>
      <category>java</category>
      <category>frontend</category>
      <category>softwareengineering</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Parts in transit - Why most distributed systems are prematurely complex</title>
      <dc:creator>Leon Pennings</dc:creator>
      <pubDate>Sun, 10 May 2026 20:29:58 +0000</pubDate>
      <link>https://dev.to/leonpennings/parts-in-transit-why-most-distributed-systems-are-prematurely-complex-378e</link>
      <guid>https://dev.to/leonpennings/parts-in-transit-why-most-distributed-systems-are-prematurely-complex-378e</guid>
      <description>&lt;h2&gt;
  
  
  The incomparability problem
&lt;/h2&gt;

&lt;p&gt;Here is a question that has no clean answer.&lt;/p&gt;

&lt;p&gt;How do you know whether the architecture you chose was the right one?&lt;/p&gt;

&lt;p&gt;Not right in the sense of working — most systems work, eventually, after enough effort. Right in the sense of optimal. Right in the sense that the complexity you introduced was warranted by the problem you were solving, and that a simpler approach would have cost more rather than less.&lt;/p&gt;

&lt;p&gt;The honest answer, in most cases, is that you cannot know. Because the alternative was never built.&lt;/p&gt;

&lt;p&gt;This is not a gap in the data. It is the mechanism of the problem. Most systems are built only once. There is no second system built with different assumptions, run for five years, and compared on total cost of ownership, ease of change, and operational stability. The counterfactual does not exist. Therefore the cost of the wrong choice — if it was the wrong choice — is permanently invisible.&lt;/p&gt;

&lt;p&gt;None of this is to say that distributed systems cannot work. Many organisations have made them function, sometimes at considerable scale — usually through exceptional engineering discipline, strong platform investment, and genuine operational maturity. The question is different: how much of the total effort, over years, went into managing the consequences of the distribution itself, rather than advancing the domain? And would a simpler boundary choice have delivered more value with less sustained overhead? The counterfactual remains hard to prove, which is precisely why we need sharper prospective indicators.&lt;/p&gt;

&lt;p&gt;And here is what makes the problem genuinely difficult: the entire industry tends to converge on the same patterns at the same time. When every team uses a similar stack, incurs similar coordination overhead, and grows to a similar size — those costs stop being visible as costs. They become the definition of what software costs. Normal and wasteful become indistinguishable.&lt;/p&gt;

&lt;p&gt;So the question sharpens. If we cannot compare architectures retrospectively, is there anything we can measure prospectively — before five years have passed — that gives us a leading indicator of whether we are building something appropriately simple, or something unnecessarily complex?&lt;/p&gt;

&lt;p&gt;There is. And it comes from an unlikely place.&lt;/p&gt;




&lt;h2&gt;
  
  
  The warehouse and the system boundary
&lt;/h2&gt;

&lt;p&gt;Consider an order fulfilment operation. An order arrives. A picker walks to the rack holding the product, picks it, and places it on the assembly line. Routine.&lt;/p&gt;

&lt;p&gt;Now consider what happens when that order is cancelled.&lt;/p&gt;

&lt;p&gt;If the picker has not yet left the rack, cancellation is a system operation. One record updated. The state change is contained. The cost is negligible and the outcome is certain.&lt;/p&gt;

&lt;p&gt;If the picker is already walking the floor — part in hand, mid-transit — the picture changes entirely. The picker must be located and reached. The instruction must be communicated and confirmed. The picker turns around, returns the part, re-shelves it in the correct position, and logs the return. The assembly line must be told the part is not coming and adjust accordingly. Each of those steps can fail. Each failure requires its own recovery. If the picker has already placed the part on the line, someone else must retrieve it, the line has already reacted to its arrival, and the cleanup compounds further.&lt;/p&gt;

&lt;p&gt;The correction costs more than the original action. Not marginally more — multiplicatively more. More people, more coordination, more opportunity for secondary failure, and a system left in a state requiring verification before it can be trusted again.&lt;/p&gt;

&lt;p&gt;This is the principle that makes architectural cost measurable before a system is built:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;As long as domain actions happen within a single system boundary, the cost of failure is a rollback. The moment actions propagate outside that boundary, the cost of failure becomes coordination.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is not a preference. It is a structural property of distributed systems, and it applies regardless of how well the coordination is engineered. You can manage the cost with better tooling. You cannot eliminate it. It is inherent to the boundary crossing.&lt;/p&gt;

&lt;p&gt;The warehouse makes this visible in a way that software obscures. In the warehouse, you can see the picker walking. You can see the empty rack. You can see the stalled line. The cost of the part in transit is physically apparent. In software, the equivalent states — the uncommitted saga step, the unacknowledged event, the stalled compensating transaction — are invisible unless you built dedicated instrumentation to see them. The cost is identical. The visibility is not. That invisibility is precisely why the cost became acceptable.&lt;/p&gt;

&lt;p&gt;The well-run warehouse minimises the time parts spend in transit, because parts in transit are the expensive state. The leading indicator of a well-designed system is the same: how much of the domain work happens within a single rollback boundary, and how much crosses outside it?&lt;/p&gt;

&lt;p&gt;Rollbackability — the degree to which a failed action can be fully undone by the system without external coordination — is a concrete, prospective benchmark for simplicity. If you are designing a system and the failure path requires coordinating compensation across multiple services, you have already committed to a significant and permanent cost. The question is whether the benefit justified it.&lt;/p&gt;

&lt;p&gt;In most cases, that question was never asked.&lt;/p&gt;




&lt;h2&gt;
  
  
  A concrete example: order creation
&lt;/h2&gt;

&lt;p&gt;Take a canonical domain flow: an order is created, inventory is reserved, an invoice is generated, a shipment is planned. Four concepts. One business action. It either succeeds completely or it does not happen.&lt;/p&gt;

&lt;p&gt;In a monolith with a well-modelled domain, this is the entirety of the orchestration:&lt;/p&gt;

&lt;p&gt;java&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="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;OrderConfirmation&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;Inventory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reserve&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="nc"&gt;Invoice&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Invoice&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="nc"&gt;Shipment&lt;/span&gt; &lt;span class="n"&gt;shipment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Shipment&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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OrderConfirmation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&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="n"&gt;invoice&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shipment&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;The database transaction is the system boundary. If anything fails, nothing happened. The domain concepts — Order, Inventory, Invoice, Shipment — do the work. The technology serves them. Rollbackability is total. The failure path costs nothing beyond the failed attempt itself.&lt;/p&gt;

&lt;p&gt;This example is deliberately straightforward — but the principle holds as domain complexity increases. In fact, the more complex the domain, the more important it becomes that the infrastructure does not add noise. A complex financial workflow with regulatory holds is hard enough to reason about correctly without the additional burden of distributed coordination, partial failure states, and eventual consistency layered on top of it.&lt;/p&gt;

&lt;p&gt;Now split those four concepts across four services. The business requirement has not changed by a single word. What changes is everything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  The infrastructure required before writing a line of business logic
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A message broker.&lt;/strong&gt; Services cannot call each other synchronously if you want any resilience. Kafka or RabbitMQ: a three-node production cluster, topic design, schema registry, retention policies, consumer group monitoring, and a local development environment every developer must run and maintain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Saga infrastructure.&lt;/strong&gt; There is no transaction. Coordination must be made durable — if the orchestrator crashes mid-flow, it must resume from the correct step. This means a saga framework (Axon, Temporal, AWS Step Functions — each a substantial system with its own operational model and learning curve) or a hand-rolled saga state table with step tracking and a crash recovery process. Either way, there is now a fifth service whose entire existence is accidental complexity. It owns no domain concept. It exists solely because the transaction boundary was removed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distributed tracing.&lt;/strong&gt; Four services produce four independent log streams with no shared identity unless you build one. Jaeger or Zipkin for the trace infrastructure. Every service propagates a correlation ID in HTTP headers, event envelopes, and log output. A log aggregation stack on top, because reconstructing an incident across four separate log streams without tooling is not a debugging workflow — it is an archaeology project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency handling — in every service.&lt;/strong&gt; Message brokers guarantee at-least-once delivery. The same event will arrive twice. Every consumer must handle this without creating two invoices or two shipments. An idempotency key strategy per event type. A deduplication store — typically a processed-events table — checked on every inbound message. This is not a framework you install. It is code you write, in every service, correctly, and maintain forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compensating transactions — per failure path.&lt;/strong&gt; The rollback equivalent. Designed, coded, tested, and maintained per service per failure scenario. For four services the paths are: inventory fails — cancel order; invoice fails — release inventory, cancel order; shipping fails — void invoice, release inventory, cancel order. Each compensation is a domain operation that must exist, be reachable, be idempotent, and be tested both in isolation and in combination. The failure paths grow as O(n²) with the number of services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API contracts and versioning.&lt;/strong&gt; In a monolith, a method signature change is a compiler error caught before deployment. Across services it is a potential production incident. OpenAPI specifications or event schemas in the schema registry. A versioning strategy for deploying new service versions while old ones are still running. Consumer-driven contract tests — an entirely new test layer that did not exist before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-service operational overhead — multiplied by four.&lt;/strong&gt; Each service needs its own CI/CD pipeline, its own database (shared databases between services defeat the architectural purpose), its own health checks, its own deployment configuration, its own secret management, and its own database migration strategy.&lt;/p&gt;

&lt;p&gt;None of this is business logic. All of it requires expertise to operate correctly. In practice it means a platform or infrastructure team to own the broker and deployment infrastructure, application developers who understand distributed systems failure modes rather than just domain logic, and an ongoing operational load that scales with the number of services — not with the complexity of the domain.&lt;/p&gt;




&lt;h2&gt;
  
  
  The cost, made visible
&lt;/h2&gt;

&lt;p&gt;The following table makes the prospective cost explicit — before the first line of business logic is written, and before five years have passed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Monolith&lt;/th&gt;
&lt;th&gt;Microservices&lt;/th&gt;
&lt;th&gt;What the split actually costs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Atomicity and failure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback on failure&lt;/td&gt;
&lt;td&gt;Database transaction. One word.&lt;/td&gt;
&lt;td&gt;Saga pattern. Hundreds of lines.&lt;/td&gt;
&lt;td&gt;Design, code, and test a compensating action per service per failure path. O(n²) paths for n services.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partial failure state&lt;/td&gt;
&lt;td&gt;Impossible. Transaction is atomic.&lt;/td&gt;
&lt;td&gt;Permanent possibility. Must be designed around.&lt;/td&gt;
&lt;td&gt;Order exists, invoice does not. Every consumer of your data now reasons about completeness. Forever.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consistency&lt;/td&gt;
&lt;td&gt;Immediate. Guaranteed.&lt;/td&gt;
&lt;td&gt;Eventual. A property you live with.&lt;/td&gt;
&lt;td&gt;Not solvable with better tooling. A structural consequence of the boundary choice.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Infrastructure before business logic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message broker&lt;/td&gt;
&lt;td&gt;None.&lt;/td&gt;
&lt;td&gt;Kafka or RabbitMQ. 3-node cluster.&lt;/td&gt;
&lt;td&gt;Topic design, schema registry, retention policy, consumer group monitoring, local dev setup.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Saga / orchestration&lt;/td&gt;
&lt;td&gt;None.&lt;/td&gt;
&lt;td&gt;Axon / Temporal / hand-rolled plus a fifth service.&lt;/td&gt;
&lt;td&gt;Durable saga state, crash recovery, step tracking. An entire service that owns zero domain concepts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distributed tracing&lt;/td&gt;
&lt;td&gt;One stack trace.&lt;/td&gt;
&lt;td&gt;Jaeger / Zipkin plus correlation IDs everywhere.&lt;/td&gt;
&lt;td&gt;Every service propagates trace IDs in headers, event envelopes, and log output. Log aggregation stack on top.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idempotency&lt;/td&gt;
&lt;td&gt;N/A. Methods are naturally idempotent.&lt;/td&gt;
&lt;td&gt;Required in every service. Always.&lt;/td&gt;
&lt;td&gt;Deduplication store per service. Idempotency key strategy per event. Written, maintained, tested forever.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API contracts&lt;/td&gt;
&lt;td&gt;Compiler. Free.&lt;/td&gt;
&lt;td&gt;OpenAPI / schema registry plus versioning strategy.&lt;/td&gt;
&lt;td&gt;Consumer-driven contract tests. A breaking change is a production incident. Another test layer that did not exist.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Per-service operational overhead&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD pipelines&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;4+&lt;/td&gt;
&lt;td&gt;Independent versioning, deployment windows, rollback strategies. Coordination overhead on every release.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Databases&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;4+&lt;/td&gt;
&lt;td&gt;Independent migration strategies per service. Schema changes coordinated across deployment boundaries.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local dev environment&lt;/td&gt;
&lt;td&gt;One process.&lt;/td&gt;
&lt;td&gt;4+ services plus broker plus docker-compose.&lt;/td&gt;
&lt;td&gt;Onboarding measured in days not hours. Partial environments produce integration bugs that only appear in the full stack.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Debuggability and sustainability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debug a production failure&lt;/td&gt;
&lt;td&gt;One stack trace. One log stream.&lt;/td&gt;
&lt;td&gt;Reconstruct a timeline across 4+ log streams.&lt;/td&gt;
&lt;td&gt;Clock skew between services. Correlation IDs that were not propagated. Broker lag that shifted event order.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bug surface&lt;/td&gt;
&lt;td&gt;Domain complexity only.&lt;/td&gt;
&lt;td&gt;Domain multiplied by accidental complexity.&lt;/td&gt;
&lt;td&gt;Each async handoff is a new class of timing bug. Compensating paths run rarely, are tested inadequately, and fail in production.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codebase legibility&lt;/td&gt;
&lt;td&gt;Domain is the code.&lt;/td&gt;
&lt;td&gt;Domain distributed across event schemas and API contracts.&lt;/td&gt;
&lt;td&gt;"What does order creation actually do?" has no single answer. The behaviour is implicit in subscriptions across four codebases.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance cost over time&lt;/td&gt;
&lt;td&gt;Proportional to domain complexity.&lt;/td&gt;
&lt;td&gt;Domain plus accidental complexity.&lt;/td&gt;
&lt;td&gt;Accidental complexity does not reduce over time. Services accumulate. Contracts fossilise. Framework versions break. Teams leave.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unit of scale&lt;/td&gt;
&lt;td&gt;The atomic action. Run more instances.&lt;/td&gt;
&lt;td&gt;Individual steps — which are not the bottleneck.&lt;/td&gt;
&lt;td&gt;Invoice creation and shipment planning are simple writes. They are not traffic hotspots. The decomposition solves a problem that does not exist.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure to scale&lt;/td&gt;
&lt;td&gt;Load balancer plus N identical instances.&lt;/td&gt;
&lt;td&gt;Everything above, multiplied.&lt;/td&gt;
&lt;td&gt;All the saga, broker, and tracing infrastructure exists solely to reconstruct what the database transaction provided for free.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The scaling argument that is rarely examined closely
&lt;/h2&gt;

&lt;p&gt;The case for microservices typically rests on scalability. You can scale the parts that need scaling independently, rather than scaling everything together.&lt;/p&gt;

&lt;p&gt;This sounds rational until you ask what actually needs scaling.&lt;/p&gt;

&lt;p&gt;In an order creation flow, the bottleneck is almost never the invoice logic or the shipment record creation. These are simple writes that happen once per order. The thing that needs scaling is the number of concurrent orders being created — the atomic action as a whole.&lt;/p&gt;

&lt;p&gt;Scaling the atomic action requires a load balancer and N identical instances of one deployed artefact. Each instance connects to one database. The database handles concurrent transactions reliably, as it has for decades. The infrastructure cost is a fraction of the distributed alternative. The operational complexity is a fraction. The failure surface is a fraction.&lt;/p&gt;

&lt;p&gt;A well-modelled core domain is not large. This is not an aspiration — it is what remains when accidental complexity is removed. The essential logic of order-to-shipment fits comfortably in one process, understood by one team. What makes codebases large is not the domain. It is frameworks imposing their structure on domain code, duplication caused by unclear boundaries, accidental complexity accreting around poor models, and boilerplate generated by architectural patterns that do not fit the problem.&lt;/p&gt;

&lt;p&gt;Strip those out and the core is small, fast to deploy, cheap to run, and trivially scalable as a unit.&lt;/p&gt;

&lt;p&gt;The industry asked "how do we scale the parts?" before asking whether the parts needed to be separate. It then built an entire ecosystem of frameworks, patterns, and operational infrastructure to answer the first question — all solving a decomposition problem that, in most cases, did not need to exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  When distribution is the right answer — and when the arguments do not hold
&lt;/h2&gt;

&lt;p&gt;Distribution has genuine use cases. They are narrower than the industry's adoption rate suggests, and several of the most commonly cited justifications do not survive close examination.&lt;/p&gt;

&lt;h3&gt;
  
  
  Physical and regulatory constraints
&lt;/h3&gt;

&lt;p&gt;The standard argument: if data must live in a specific jurisdiction for regulatory reasons, you need a distributed architecture.&lt;/p&gt;

&lt;p&gt;The better answer: replicate the full domain logic into that regulatory cell. The atomic action stays atomic. The cell — with its own deployment, its own database, its own complete stack — is the unit of distribution. What you do not do is split the domain action across a jurisdictional boundary, routing parts of it between regions. That creates the coordination cost of distribution without the isolation that justified it. The constraint is geographic. The solution is geographic deployment of the whole, not decomposition of the parts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Independent scaling profiles
&lt;/h3&gt;

&lt;p&gt;The standard argument: if one component needs more scale than others, separating it avoids scaling everything unnecessarily.&lt;/p&gt;

&lt;p&gt;The better answer: the cost of splitting a single component out of an otherwise coherent domain action is large, fixed, and permanent — as the table above makes clear. The question is not only "does this component need more scale?" but "does the benefit of isolating its scale exceed the full coordination cost of the split?" In most cases it does not, because the component that appears to need independent scaling is rarely the actual bottleneck under measurement, and because scaling the whole is cheaper than the industry assumes. If there is no compelling reason not to scale everything, scale everything. Simplicity requires a reason to abandon it, not a reason to adopt it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Organisational boundaries
&lt;/h3&gt;

&lt;p&gt;The standard argument: Conway's Law — systems tend to mirror the communication structures of the organisations that build them. If teams are separated, align the architecture accordingly.&lt;/p&gt;

&lt;p&gt;Conway's Law is a useful observation in retrospect. It describes what tends to happen when architecture is not deliberately managed. It is not a prescription, and it should never be used as one. Using it as a justification for a service boundary is encoding organisational structure permanently into the system — and paying the technical cost of that boundary in every sprint, by every developer, for the lifetime of the product.&lt;/p&gt;

&lt;p&gt;The cost of an artificially introduced service boundary compounds over years. The cost of reorganising a team is paid once. The engineering should define the ideal architecture with as few compromises as possible. The organisation should be arranged to serve that architecture, not the other way around. This pays dividends — perhaps not in year one, but reliably by year five, and every year thereafter. Teams that succeed with microservices often do so despite the architecture, through heroic platform investment and operational discipline. The patterns can be made to work. The deeper question is whether they were the right starting point for the domain in front of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Genuinely independent domain concepts
&lt;/h3&gt;

&lt;p&gt;This is the one case where distribution has a legitimate technical argument — and even here, the bar should be high.&lt;/p&gt;

&lt;p&gt;Domain concepts are genuinely independent when they have no transactional relationship with each other. Not merely different in name or ownership, but different in the sense that one completing or failing has no bearing on the integrity of the other. A recommendation engine and a payment processor are genuinely independent. An order and its invoice are not.&lt;/p&gt;

&lt;p&gt;The strongest version of this argument comes from systems with a fundamentally asymmetric workload — a platform where reads vastly outnumber writes, where the read path has no transactional requirement, and where the scale difference between the two is large and proven. A social platform where the overwhelming majority of requests are reads with no transactional requirements is a system where isolating the read path separates two genuinely different kinds of work with different resource profiles and different failure tolerances.&lt;/p&gt;

&lt;p&gt;But this is a workload argument supported by measurement, not an architectural principle applied by default. It applies to a small fraction of the systems that have adopted microservices, and it should be reached by evidence, not anticipated in advance.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three tests before splitting a boundary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The rollback test.&lt;/strong&gt; If this action fails halfway through, what does recovery cost? If the answer is a database rollback, the action belongs inside a single boundary. If the answer is a coordinated sequence of compensating calls across multiple services, each of which can itself fail, ask whether that coordination cost was consciously accepted — or simply inherited from a pattern that was never examined.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The scaling test.&lt;/strong&gt; Which specific step in this action is the measured bottleneck under current or near-term load? Not the theoretical bottleneck. The step that is demonstrably the constraint today, under real conditions. If the answer is none of them individually, the action does not need decomposition. It needs more instances of the whole.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The standup test.&lt;/strong&gt; In the daily standup, what language does the team use? If the items are about services, pipelines, brokers, schemas, and migrations — the team is working on accidental complexity. If the items are about domain concepts — what an order means, who owns a responsibility, what a rule actually requires — the team is working on the right problems. You do not need a cost model to apply this test. You need one conversation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Measuring it in a system you already have
&lt;/h2&gt;

&lt;p&gt;If these tests apply prospectively, they also apply to systems already in production. A short audit reveals more than any architecture review.&lt;/p&gt;

&lt;p&gt;Count the sagas. How many business capabilities require a saga or orchestrator to complete? Each one is a boundary crossing that converted a rollback into a coordination problem. The number tells you how much of the domain is currently in transit.&lt;/p&gt;

&lt;p&gt;Measure the standup ratio. Over two weeks, track how many standup items are about infrastructure, services, pipelines, and schemas versus domain concepts, rules, and business questions. The ratio is a direct reading of how much of the team's daily energy is absorbed by accidental complexity.&lt;/p&gt;

&lt;p&gt;Trace a failure end to end. Pick a recent production incident. Count the number of log streams, services, and correlation IDs required to reconstruct what happened. That reconstruction cost — in time, in tooling, in expertise — is paid on every incident. It is the maintenance tax of the boundary choices made at design time.&lt;/p&gt;

&lt;p&gt;Apply the migration heuristic. A well-modelled monolith can be split later, when measurement proves a specific boundary is warranted. A distributed system can rarely be reassembled cheaply once the boundaries have fossilised into contracts, event schemas, and separate team ownership. Optionality has value. The simpler starting point preserves it. The complex starting point spends it immediately, in exchange for flexibility that may never be needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  First principles
&lt;/h2&gt;

&lt;p&gt;There is nothing novel in the argument this article makes. It is an application of principles that engineering has held for as long as engineering has existed.&lt;/p&gt;

&lt;p&gt;Minimise the moving parts. Every component that can fail will eventually fail. Every interface between components is a surface for misunderstanding, for version drift, for timing errors that only appear under conditions nobody anticipated. The system with fewer moving parts is not the primitive system — it is the disciplined one.&lt;/p&gt;

&lt;p&gt;Solve the problem in front of you. The system that is over-engineered for scale it has not reached, for distribution it does not need, for independence that its domain does not have — that system is not prepared for the future. It is burdened by it. It is paying, today and every day, for problems it may never have.&lt;/p&gt;

&lt;p&gt;Prefer reversibility. The decision that can be undone when it proves wrong is worth more than the decision that cannot, regardless of how confident you are at the time. A monolith that can be split later, when the evidence demands it, is a better starting point than a distributed system that cannot be reassembled after the evidence proves the split was premature.&lt;/p&gt;

&lt;p&gt;Measure before you commit. The incomparability problem — the fact that the alternative architecture was never built, so its cost can never be directly compared — cannot be fully solved. But its worst effects can be mitigated by demanding evidence before committing to complexity: evidence of the scaling requirement, evidence of the domain independence, evidence that the coordination cost is worth the benefit it buys.&lt;/p&gt;

&lt;p&gt;The software industry has a habit of adopting solutions before fully understanding the problems they were designed to solve, and then normalising the cost of those solutions until the cost becomes invisible. The distributed systems patterns that dominate today were developed by organisations with genuine physical distribution requirements, at a scale that a small fraction of systems ever reach. They solved real problems. They are also expensive, complex, and failure-prone in ways that compound over time and rarely appear on the original architectural diagram.&lt;/p&gt;

&lt;p&gt;The question to ask, before any architectural decision, is not "how do others solve this?" It is "what does this problem actually require?" Start from first principles. Follow the cost. Build the simplest thing that genuinely solves the problem in front of you. Treat every boundary crossing — every point where a database rollback becomes a distributed coordination problem — as a commitment with a known, permanent price tag.&lt;/p&gt;

&lt;p&gt;Because it will cost exactly that. Invisibly, continuously, and for as long as the system runs.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>java</category>
      <category>microservices</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
