There are two fundamentally different levels in software development.
Level 1 — Getting the system to run
At this level the goal is straightforward:
the application compiles
the system deploys
features behave as expected
users can operate the software
If those conditions are met, the project is considered successful.
Most modern frameworks are extremely good at helping teams achieve this level. A stack such as Spring Framework provides a ready-made structure for building applications quickly: web infrastructure, dependency injection, persistence integration, configuration management, and more. With the right templates and tooling, teams can produce a working system in relatively little time.
Level 2 — Keeping the system economically evolvable
The second level is far harder.
Once the system has been running for several years, the real question becomes:
Can developers still reason about the business rules?
Can features be added without breaking existing behavior?
Does the cost of change remain predictable?
This is the level where software must remain economically viable. The system must evolve along with the business without collapsing under its own complexity.
Most of the industry focuses almost entirely on Level 1, because Level 2 is much harder to see.
The Observability Problem
In many engineering disciplines, different designs can be compared directly.
Two airplane designs can be tested against each other. Two bridge designs can be analyzed under the same load conditions. Engineers can evaluate alternatives objectively.
Enterprise software is different.
Most systems are unique implementations of a specific business domain. A logistics system for one company will not be rebuilt several times with different architectures just to compare which approach works best.
Because of that, organizations rarely observe multiple competing implementations of the same domain.
There is no side-by-side comparison like:
System A — rich domain model
System B — procedural service architecture
running the same business processes for several years.
Instead there is only one system: the one that was built.
As a result, success is evaluated using the only clearly visible metric:
Does the application run?
If the answer is yes, the architecture is usually considered successful.
The Rise of the Software Factory
Over time, the industry optimized for what was easiest to measure: producing working applications quickly.
This led to the emergence of standardized software production lines.
Typical enterprise stacks often follow a very familiar structure:
controller
service
repository
database
Add REST APIs, containerization, messaging infrastructure, and often microservices, and a predictable pattern emerges.
Framework ecosystems reinforce this pattern. They provide conventions, templates, and project generators that make it easy to spin up new services quickly.
From an organizational perspective, this approach promises several advantages:
developers can move between projects easily
teams can scale rapidly
systems follow familiar patterns
onboarding new engineers becomes simpler
These promises are appealing, especially to organizations managing large engineering teams.
However, these advantages are rarely tested against alternative architectural approaches. Because most enterprise systems are built only once, there is no direct comparison showing whether a different design would actually have been more efficient or easier to evolve.
As a result, the perceived success of factory-style development often rests on a simple observation:
The application runs
Features are delivered
Without a comparable implementation built around a different architectural philosophy, it is difficult to see whether the chosen approach truly delivered its promised benefits.
The result is a development process that resembles a software factory: a standardized production line designed to produce working applications quickly.
Whether it produces the right kind of system for the domain is a different question entirely.
The Model T Problem
The logic behind this standardization is similar to the philosophy associated with Henry Ford and the Ford Model T.
The Model T revolutionized manufacturing through standardization. One of the famous ideas attributed to Ford was that customers could choose any color, as long as it was black.
This approach worked because the product itself was standardized.
Cars were produced for a broad market with relatively similar requirements.
Enterprise software is fundamentally different.
Each system represents a specific business domain:
logistics operations
insurance policies
trading platforms
healthcare workflows
These domains have very different requirements and behaviors.
In effect, each enterprise application needs a different type of vehicle.
Some domains resemble heavy trucks carrying complex transactional logic. Others behave more like high-performance machines, where performance and precision matter enormously. Some systems are small and lightweight.
Yet many development ecosystems attempt to solve all of them with the same architectural pattern — the equivalent of producing a Model T for every possible use case.
Why This Appears to Work
Despite the mismatch, the Model T architecture still appears successful.
After all, a Model T can still move forward. It can transport people and even carry small loads.
Similarly, standardized enterprise architectures can deliver features:
endpoints respond to requests
data is stored and retrieved
workflows execute
From the outside, the application works.
Because organizations rarely build the same system twice with different architectures, they never see a direct comparison. There is no competing design demonstrating that the system could have been far simpler or easier to evolve.
As long as the application runs and delivers features, the architecture appears to perform as expected.
The Hidden Cost of Factory Style Development
The real cost of factory-style architectures emerges gradually and usually in two dimensions: effort and functional quality.
Effort: Why development gets slower over time
In factory-style systems, implementing new functionality tends to require roughly the same effort every time.
Every feature follows the same pattern:
controller
service
repository
integration logic
Because the architecture is primarily procedural, the system rarely accumulates reusable domain behavior. Each feature often introduces new service logic rather than building upon existing concepts.
As the system grows, the situation frequently worsens:
similar logic appears in multiple services
developers must read many parts of the system to understand behavior
debugging requires tracing through multiple layers and integrations
knowledge transfer becomes difficult
The effort required to implement new functionality often remains constant or even increases over time.
Function-driven systems behave very differently.
When a system evolves around a coherent domain model, the model itself becomes a growing knowledge base of the business. Domain objects accumulate responsibilities and reusable behavior.
As the model matures:
new features often extend existing objects
behavior is reused rather than reimplemented
developers can understand the system by understanding the model
Knowledge transfer becomes easier because the model tells the story of the application.
Debugging is also simpler. When rules live in their responsible objects, it becomes immediately clear where behavior originates. Developers do not need to search across multiple services implementing slightly different versions of the same logic.
Over time, the effort required to add functionality tends to decrease, because the model provides increasing leverage.
Quality: One version of the truth
Factory-style architectures often distribute business rules across multiple services.
It is common to find logic that is similar but not identical in different places:
slightly different validation rules
small variations in calculations
edge cases handled in one service but not another
These inconsistencies are rarely intentional. They appear gradually as new features are implemented independently.
The result is a system with multiple interpretations of the same business rule.
Function-driven systems address this differently.
Each business rule belongs to the object responsible for that concept. The rule has one canonical implementation.
If a system contains an Order concept, the logic related to orders lives with the Order object. If there is pricing logic, it belongs to the pricing model.
This creates a single version of the truth.
Rules are not scattered across services or hidden inside orchestration layers. They are located where the business concept itself lives.
This greatly reduces contradictions and makes the system far easier to reason about.
The Engineering Principle
Enterprise systems should be function-driven rather than tool-driven.
Architecture should begin with understanding the domain:
the concepts involved
the relationships between them
the rules that must remain consistent
Only after that understanding emerges should tools and frameworks be introduced to support the system.
Tools are valuable when they solve real problems in the running application:
persistence
messaging
scaling
reliability
But they should not dictate the structure of the domain model.
Why Rich Domain Models Help
A function-driven system begins with the domain model, not with architectural patterns or infrastructure.
Instead of starting with decisions like:
microservices
event-driven architecture
CQRS
messaging platforms
development begins with understanding the domain itself.
The first goal is to model the core concepts and their responsibilities.
Typical objects might represent concepts such as:
Order
Customer
Shipment
Invoice
These objects contain the behavior that defines the business logic.
At this stage, the focus is entirely on implementing the core functionality of the application.
Infrastructure concerns are introduced only when they become necessary.
For example:
persistence is added when data must be stored
messaging appears when asynchronous coordination is required
scaling mechanisms appear when load actually demands them
In practice, many enterprise systems never reach the scale that requires complex distributed architectures.
For the majority of applications, a well-designed domain model within a cohesive system is entirely sufficient.
Only when real operational constraints appear should the architecture evolve technically.
This approach keeps the system aligned with the domain while avoiding premature technical complexity.
The result is software that grows organically around the business model, rather than being constrained by predefined architectural templates.
Closing Thought
TThe software industry has become extremely good at producing applications that run.
Frameworks, templates, and standardized stacks make it possible to build complex systems faster than ever before.
But enterprise software is not a short-term product. It is a long-lived system that must evolve together with the business.
Designing such systems requires something different from a software factory. It requires starting with the domain, building a coherent model, and letting the architecture grow from the problem instead of from the tools.
Otherwise we keep producing the same solution for every problem:
another black Model T.
The problem with that approach is not aesthetic. It is economic.
When the architecture does not match the domain, the mismatch shows up in three places:
more engineering effort to implement and understand functionality
higher long-term costs as development slows and operational complexity increases
more fragile systems where business rules are scattered and difficult to reason about
In other words, the wrong architectural vehicle does not merely look inelegant — it makes the system harder and more expensive to operate for the rest of its lifetime.
Long-lived enterprise software requires something better than a one-size-fits-all production line.
It requires architectures that are designed around the domain they serve.
Top comments (0)