Everything will work
In application development, and probably in every IT field, almost any tool will get you to a working result. Any tool, any combination of frameworks and technologies can be strung together and made to work. If the bar is 'does it run and do what was asked', nearly every approach clears it.
That is exactly why it cannot be the goal. A criterion that every approach passes cannot tell good approaches from bad ones. Working is the entry criterion. The real work starts afterwards: how flexible is the code, how easy is it to introduce a domain change, and how quickly does that change become real?
The market behaves as if getting it to work is the core business, preferably with as little coding as possible. But 'as little coding as possible' is measured in the lines you write. Underneath, stacked frameworks cause an explosion in the code, configuration and behavior that someone has to understand later.
A requirement is rarely the whole picture. Think of it as the description of a toe: accurate as far as it goes, while the thing being built is an elephant. Early on, the elephant is unknown, although a lot is known about toes and feet. Building and discussing is how the rest comes into view, and that never really ends: the more of the domain is understood, the better the software can fit it.
Two approaches
The first question is how you set up your development process.
The requirement is a contract. A requirement or user story comes in, you build exactly what was specified, and working code comes out. The implementation is a delivery problem.
The requirement is the description of a toe. You are trying to discover the animal, and the implementation is part of the search. Every input (a story, old code, a database, a report, a passing remark) is a hypothesis to verify with the domain expert: assume, then verify.
The contract model is never the fastest nor is it the most productive option. A single requirement can be satisfied in many ways, and a contract implementation simply picks one: a solution for toe 1. A search for what the thing actually is builds a structure that fits the animal. When the next requirement arrives, that structure is already closer to it, and by the time the long nose shows up it is far closer to the elephant than the toe 1 solution ever was. Each requirement then takes progressively less work.
Take a library that starts by lending books. Then comes 'we also want to lend DVDs'. As a contract, that is a ticket: add a DVD with a title, director and runtime, and close it. Magazines and tools follow the same way, until there are four parallel kinds of item, duplicated logic, and reports that have to stitch them together. As a search, the DVD is new information about the domain: the library doesn't lend books and DVDs differently, the business domain is lending of items. The model changes to match. The Book object gets refactored into an Item object with an ItemType. The next type, whatever it is, can use the existing item-lending structure without introducing a new functional silo.
Write to adapt
Architects and engineers are not domain experts, so their crafted solutions and models should be assumed suboptimal. That leaves a choice: take the suboptimal structure as fixed and encode it, or make changing it cheap. Choosing the second does not mean writing suboptimal or unfinished code. It means a lean architecture with as little boilerplate as possible.
Lightweight choices are no loss anyway: fewer gears to maintain, fewer places for something to go wrong, and less structure to misinterpret or misuse.
The model will always change, and not only at the start. Developers have to become domain experts too: as understanding of the animal grows, the domain fits the code better and better, and that never ends. Early conversations with domain experts tend to stay theoretical. Once more becomes visible in the UI, there is more to discuss and more to learn. That churn is not a cost of being wrong. It is how the domain gets discovered.
Coding is testing: if the model works, the code is simple, and if the code is complex, the model needs adjusting.
Boilerplate, redefined
In the market, boilerplate means typing: getters, setters, builders, anything a tool or an AI can generate for you. Remove the typing and you have supposedly removed the boilerplate.
A better definition exists, and Fred Brooks supplies the vocabulary. In 'No Silver Bullet' he separated essential complexity, which is inherent in the problem, from accidental complexity, which comes from how the solution is built. Boilerplate, defined that way, is accidental complexity code: code that serves the machinery rather than the domain. Dependency injection is the typical example.
A getter with business meaning is not boilerplate. Whether a property can be read or modified from outside an object is part of its contract. A field without a getter is hidden; a field without a setter cannot be arbitrarily changed by external callers. Those are encapsulation decisions, not typing chores.
Generating getters and setters for every field is therefore not the elimination of boilerplate. It is the elimination of a design decision. The need for such indiscriminate generation is itself a signal that the model is being treated as a data structure rather than an object that owns behavior and protects its invariants.
Making accidental code cheaper to write does not remove it. It makes it cheaper to produce more of it, while the cost of understanding and maintaining it stays the same. Every supporting framework also adds a standing tax: the domain does not change when a framework ships a major version, but you pay for the migration anyway.
Accidental complexity risks obscuring essential complexity. When architectural machinery separates behavior from the domain concepts it represents, the technology gets in the way of understanding the domain. The context that gives the code its meaning becomes harder to see.
One test works for every tool: does it enhance the essential complexity, or make it more visible?
Hibernate, for instance, passes. Mapping a result set onto objects has to happen whether anyone likes it or not, and with explicit annotations the column names stay readable right on the entity. The accidental work is absorbed and nothing the domain reader needs is hidden.
Many frameworks do not seem to pass. They encourage an accidental setup; the domain way of working encourages an essential one. Dependency injection frameworks, for example, mostly solve a wiring problem the design itself creates. Ask yourself: does the framework provide something the domain model actually needs, or is it solving a problem that isn’t present and has no home in the business process? Is it mainly solving the architecture and technology problems that the framework itself creates and markets?
None of this is about what is possible. Applications built with frameworks work. It is about what they cost once they work: the application becomes more complex, the framework adds more code and configuration on top, and every supporting framework brings a standing tax in updates and migrations. As long as a framework enhances the essential complexity rather than scattering it, its benefits outweigh the maintenance tax.
Why you need a rich domain model
A rich domain model does two things. First, it centralizes and contextualizes what happens where. A rule lives in one place, on the entity that owns the concept.
Take the loan period in that library. It is not a property of an item. It is a policy with its own identity: seven days for ItemType DVD, twenty-one for ItemType books, an exception for one rare edition, all configurable without code changes. The rule lives in one place, and changing it means changing that one place.
Second, a rich model gives you something to discuss. When the code is plain, verbose Java with behavior on the entities, domain experts can largely read and follow it. If they can look at something like member.canBorrow(book) and say 'no, that's not how it works', the code has become a second window onto the domain. The model gets explained to the experts as the work goes, both for feedback and so that they learn from it.
A rich model also lets opportunities emerge. Record a loan as a first-class concept, with its own start, end and return date, and the model can already answer questions nobody asked for: how often a title was borrowed last year, whether it is borrowed back to back and a second copy is worth buying, which items are overdue, which member has the most active loans. None of that needed a new story or a change to the model. It is also the onboarding path. Developer efficiency is a functional barrier, how fast someone understands the domain, not a technological one, and a 30-minute whiteboard session plus the model itself covers that.
Why the UI is the first and most important deliverable
A real UI should come as early as possible, even if the application doesn't really require one. Not wireframes: a working UI.
It is the point-and-discuss tool. Many domain experts cannot visualize or theorize a domain in their head. They need something visual to point at. And you don't want to discuss logging with your product owner, or in your demos. You get feedback when things are tangible. The UI is the feedback mechanism for the encoded domain, and it makes it possible to build the first model underneath it with real-time feedback.
There is a risk that experts anchor on screens instead of the model. In practice that risk is limited, especially when the model underneath is readable enough for them to follow what the screen is showing.
Where it matters
It all works. For small applications, where no context barriers are broken, the difference doesn't matter much. But as soon as the 'small' label disappears, the difference in approach can save a lot of work, plausibly multiple orders of magnitude. That effort estimate comes from more than 25 years of building the 'search' way and reviving stalled projects that were implementing the 'contract' way.
This approach is valid for basically every application imaginable. The starting point stays the same: not everything is known up front, so the structure has to stay open to change. And simple, flexible code is not a disadvantage in any scenario, so the approach has only advantages.
Getting it to work gets you in the door. What happens after that is the actual work.
Top comments (0)