Tools
How to Build Long-Living Software Systems
2025-12-14
0 views
admin
Longevity Is an Evolutionary Property ## User Stories Are Learning Instruments, Not Work Orders ## Model First, Implementation Second ## Anemic Domain Models Are a Symptom, Not the Root Cause ## Framework-Driven Development and Forced Churn ## Transactions Are Domain Decisions, Not Technical Ones ## Evolutionary Domain Development Is Faster, Not Slower ## Strong Teams vs. Guardrails ## Review the Model, Not Just the Code ## The Differentiator ## In Closing Most software systems do not fail because of bad technology choices.
They fail because they stop learning. Frameworks age. Architectures fall in and out of fashion. Teams change. None of that is fatal by itself. What is fatal is when a system’s internal understanding of the business slowly diverges from reality—until even small changes become risky, expensive, and unpredictable. At that point, rewrites are proposed. New architectures are introduced. New frameworks are adopted. Yet the underlying problem persists, because it was never technical to begin with. Long-living systems are not built by choosing the right framework or delivery methodology. They are built by continuously validating and evolving the domain model, keeping it aligned with the business domain it represents. That requires a different way of thinking about user stories, modeling, and implementation. A long-living system is not one that remains unchanged. It is one that can change continuously and incrementally without destabilizing itself. Such systems tend to share a number of characteristics: New functionality can usually be added locally Existing behavior rarely breaks when features are introduced Developers reason about the system in domain terms, not technical ones Large rewrites are unnecessary, even after years of evolution The codebase reflects how the business actually operates and speaks These properties do not emerge accidentally. They are the result of a system that evolves its conceptual model at the same pace as the business itself. When that evolution stops, conceptual tension accumulates: responsibilities no longer align cleanly with concepts, and changes begin to cut across unrelated parts of the system. Eventually, incremental change becomes impossible and revolutionary change appears inevitable. A user story is a flat depiction of a slice of functionality the system should support. It captures what is desired, not how responsibilities should be structured internally. Treating user stories as work orders—items to be implemented as efficiently as possible—is one of the fastest ways to erode a domain model. A more productive framing is this: Each user story is a domain validation step. Before implementation begins, teams should ask: How does this story fit the current domain model? Which existing objects, by their original definition, should participate? Does this story reveal a missing concept or responsibility? Do existing responsibility boundaries still hold? If the model absorbs the story naturally, implementation is usually straightforward.
If it does not, that friction is not an implementation problem—it is a modeling signal. Skipping this step does not eliminate work. It merely shifts responsibility decisions into services, workflows, and orchestration code, where they quietly undermine the model. Over time, the domain model stops governing behavior and degrades into a passive data schema. “Model first” does not mean designing everything up front or modeling in abstraction from reality. The domain model is the primary artifact Implementation exists to serve the model Responsibility boundaries drive code structure Technical convenience never overrides conceptual correctness Frameworks, libraries, and architectures should be interchangeable over time.
A domain model that accurately reflects the business is not. When implementation convenience dictates where logic lives, responsibility begins to drift. Over time, the model becomes decorative: it still exists, but no longer governs behavior. That is how anemic domain models emerge. An anemic domain model is often described as “entities without logic.” This definition is incomplete and misleading. A domain model is not merely a set of stateful objects. It is a structure of responsibilities. Some responsibilities require state; others do not. State is a consequence of responsibility, not its definition. Treating state as primary and responsibility as incidental inverts the model and guarantees decay. You can have logic in your objects and still have an anemic model if: Decisions are made outside objects that own the relevant responsibility, regardless of whether those objects hold state Logic is implemented outside objects that define the conceptual boundary, even when those objects explicitly exist in the model Invariants are enforced procedurally rather than structurally, through workflows or services instead of through the model itself Domain objects are reduced to coordination artifacts, while meaningful behavior is pushed into external orchestrators This is a subtle and dangerous failure mode. It is easy to introduce when the meaning and responsibility of objects are not clearly understood. Consider a domain interaction object. Such an object may exist to define: When and how the domain is entered Which consistency or transactional scope applies Which invariants must hold across the interaction It may have little or no persistent state, yet it owns critical responsibilities. If transaction management or consistency boundaries are introduced outside that interaction—at the service or infrastructure level—the object has not lost state; it has lost meaning. It still exists, but no longer governs what it was created to represent. Anemia, in this sense, is not about missing methods. It is about responsibility being displaced from the model into other parts of the code, often in the name of technical convenience or incomplete understanding. This is not about procedural implementation per se; it is about displacing responsibility from the object that owns it into multiple unrelated locations. Frameworks exert strong gravitational forces on how systems are shaped, particularly in enterprise environments. Spring Boot is a relevant example. Choosing Spring Boot is, in effect, committing to a yearly framework update cycle, and in many enterprise contexts, to a recurring licensing cost. This forced churn consumes engineering attention regardless of whether the business domain itself has changed. Even in stable domains, teams must continually: Adapt to framework evolution Re-validate infrastructure concerns This diverts focus away from the domain model. More importantly, the way Spring is typically used strongly promotes procedural, service-centric design. While Spring does not mandate this outcome, its defaults and ecosystem strongly incentivize it. In practice, most Spring-based systems follow prescribed templates: Dependency-injected orchestration Transactional workflows Passive domain objects The result is predictable: fragmented responsibility and anemic models. A particularly damaging form of conceptual erosion appears around transaction management. Transactions are routinely treated as a technical concern—something to be configured, annotated, or tuned at the service level. In reality, a transaction defines a business-level consistency boundary. It answers questions such as: What constitutes a meaningful unit of work? Which changes must succeed or fail together? When may intermediate state be observed? What does “all-or-nothing” actually mean in this domain? These are domain questions. When transaction boundaries are introduced outside the objects that define the domain interaction, the model loses ownership of a core responsibility. The domain may still describe what happens, but it no longer governs how consistency is maintained. At that point, transaction management becomes an afterthought, and the model is anemic by definition. The system may remain technically correct, but it becomes conceptually unstable. There is a persistent belief that continuous modeling slows delivery. In practice, the opposite is often true. Teams that deliberately practice evolutionary domain development—treating each story as a modeling opportunity—tend to deliver functionality significantly faster. This speed emerges from: Clear responsibility boundaries Obvious placement of new behavior Reduced coordination logic Each story reinforces the model instead of eroding it. Understanding compounds. Speed follows. Not all teams need the same level of explicitness. Teams with strong conceptual thinkers: Maintain alignment through conversation Detect responsibility drift intuitively Rarely need written definitions Most teams are not there. For those teams, guardrails are necessary: Written definitions of what each domain object represents Explicit responsibility boundaries Clear statements of what an object does not own This is not bureaucracy. It is scaffolding. As conceptual maturity increases, guardrails can be relaxed. Removing them prematurely guarantees drift. If responsibility drift happens at the conceptual level, code reviews are structurally incapable of detecting it. Code reviews excel at catching local issues: syntax errors, naming inconsistencies, missing null checks. They function much like spelling checks—useful, but limited. They give the appearance of rigor without addressing the actual source of decay. No meaningful design flaw caused by misaligned responsibilities is discovered by reading diffs in isolation. The time spent on code review is far better invested elsewhere. A more effective alternative is a model-centric review. Instead of asking “what code changed?”, ask: What changed in the domain model? Which responsibilities moved or expanded? What did we learn about the domain from this story? Have developers explain changes in words, without code. If nothing changed in the model, that should be explicit—and often that explanation reveals that something should have changed. This turns review from compliance sign-off into shared understanding. It replaces drama and ritual with learning. A rich domain model uniquely supports evolutionary growth because it provides a durable, contextualized representation of business understanding that scales gracefully with complexity. Whether the model comprises 20 or 200 objects, the cognitive load remains manageable: each concept owns its responsibilities and relationships, allowing developers to navigate and extend the system through domain language rather than scattered execution flows. In procedural or service-centric approaches, equivalent complexity explodes into an un-overviewable web of orchestration code. New insights have nowhere natural to land, turning evolution into mere accumulation of patches. A living domain model, by contrast, turns improved understanding into structural refinement—making the system not just extensible, but capable of genuinely learning alongside the business. In this sense, it finally delivers on the long-promised ideal of self-documenting code. An evolving domain model grows with the business and becomes smarter over time. That is its defining strength—and what fundamentally sets it apart from procedural or functional approaches. Procedural and service-centric systems implement the domain as it is understood today. Logic is distributed across workflows, services, and coordination layers, with little or no explicit representation of why that logic exists. There is no durable context beyond execution order. As understanding improves, incorporating that insight is not a matter of refining a model—it requires redistributing logic across an already fragmented structure. At that point, improvement becomes structurally difficult. The system can be extended, but it cannot easily learn. Evolutionary change turns into incremental complication. A living domain model, by contrast, provides a tangible representation of the business: its concepts, responsibilities, and invariants made explicit and navigable. Because responsibilities are owned and contextualized, the system maintains a total overview of what exists, why it exists, and how it relates. That overview is what makes evolutionary change possible. This is not merely an academic distinction. Long-lived applications depend on it. Code can be rewritten.
Frameworks can be replaced. But without a living, evolving domain model, improved understanding of the business has nowhere to go—and systems stagnate long before they technically fail. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse - New functionality can usually be added locally
- Existing behavior rarely breaks when features are introduced
- Developers reason about the system in domain terms, not technical ones
- Large rewrites are unnecessary, even after years of evolution
- The codebase reflects how the business actually operates and speaks - How does this story fit the current domain model?
- Which existing objects, by their original definition, should participate?
- Does this story reveal a missing concept or responsibility?
- Do existing responsibility boundaries still hold? - The domain model is the primary artifact
- Implementation exists to serve the model
- Responsibility boundaries drive code structure
- Technical convenience never overrides conceptual correctness - Decisions are made outside objects that own the relevant responsibility, regardless of whether those objects hold state
- Logic is implemented outside objects that define the conceptual boundary, even when those objects explicitly exist in the model
- Invariants are enforced procedurally rather than structurally, through workflows or services instead of through the model itself
- Domain objects are reduced to coordination artifacts, while meaningful behavior is pushed into external orchestrators - When and how the domain is entered
- Which consistency or transactional scope applies
- Which invariants must hold across the interaction - Adapt to framework evolution
- Address deprecations
- Re-validate infrastructure concerns - Stateless services
- Dependency-injected orchestration
- Transactional workflows
- Passive domain objects - What constitutes a meaningful unit of work?
- Which changes must succeed or fail together?
- When may intermediate state be observed?
- What does “all-or-nothing” actually mean in this domain? - Clear responsibility boundaries
- Obvious placement of new behavior
- Localized change
- Reduced coordination logic
- Lower cognitive load - Maintain alignment through conversation
- Detect responsibility drift intuitively
- Rarely need written definitions - Written definitions of what each domain object represents
- Explicit responsibility boundaries
- Clear statements of what an object does not own - What changed in the domain model?
- Which responsibilities moved or expanded?
- What did we learn about the domain from this story?
how-totutorialguidedev.toai