Architecture Is an Escalation Path, Not a Starting Template
How I introduce structure only when it starts paying for itself
Are Hexagonal Architecture, Clean Architecture, Onion Architecture, Vertical Slice Architecture, DDD, CQRS, and domain events all competing alternatives?
I don’t really think so. At least not all the time.
I find it more useful to see them as possible next steps in an escalation path. And there are actually two different directions in which the design can grow.
We can either model the domain side more explicitly or structure the application concern around use cases.
This distinction helps me a lot, because not every architecture discussion is about the same kind of problem:
Sometimes we need a better domain model.
Sometimes we need better application boundaries.
Sometimes we need both.
And sometimes we need neither.
Start with a vertical slice
For most new features, I like to start with a vertical slice. It keeps the use case understandable and related code close.
For example:
features/
activateMembership/
ActivateMembershipController
ActivateMembershipRequest
ActivateMembershipHandler
ActivateMembershipResultSuch a slice may also contain the persistence access needed by that feature. Or a small domain object. Or it writes everything into the controller if it’s a simple query.
In the beginning, we should have an obvious starting point. Software should be soft - so we need to ensure that it can be refactored later.
The important part here is that the feature can be quickly found and understood via a business-domain-driven folder name.
When I want to understand ActivateMembership, I should not have to reconstruct it from unfocused controller, service, repository, mapper, DTO, and test classes spread across the whole codebase.
That does not mean everything must stay in one file. And it also does not mean that Vertical Slice Architecture promotes anemic domain models and procedural code. Instead, it keeps the flow visible and aims for feature locality and should be evolved only if the complexity of the problem warrants it.
As such, vertical slices are the best starting point for any new feature as long as you realise that it needs to be evolved according to established software practices later on when the software grows in complexity.
Escalation path 1: the domain side
The first version of a vertical slice may have almost no real domain structure. The handler loads data, checks a few conditions, changes some state, stores the result, and returns a response.
For simple use cases, that can be perfectly fine. Not every feature needs a rich domain model. But sometimes the logic starts to change:
Business rules repeat.
State transitions become more important.
The same if-else checks are duplicated in several use cases.
Invalid states become too easy to create due to complex conditionals.
The code talks in technical terms, while the business talks in real-world concepts.
At that point, a small domain model can help. Not a full tactical DDD model. Not aggregates, factories, domain services, value objects, and events everywhere. But maybe just one or two objects with meaningful behaviour.
For example, instead of changing a membership status from the outside, the model may expose a method like:
membership.reactivateAfterPayment(...)Now the use case still orchestrates the flow. But the domain model starts to protect the business decision.
If the domain keeps growing, the next step may be a richer domain model with more invariants, lifecycle rules, value objects, intention-revealing methods. We may even add aggregates, if consistency boundaries become important.
And if we are working in an event-driven or event-sourced system, or simply want to be able to talk about something meaningful that happened in the system that another part of the system should react to, we can add domain events.
But again, this is and should not be the default for every system. It becomes only useful when the business rules deserve that level of protection.
So on the domain side, the escalation may look like this:
No real domain structure
→ small domain model
→ richer domain model
→ aggregates / value objects / domain events where usefulThe important questions are:
Are we just moving data around?
Or are we protecting business behaviour?
Escalation path 2: the application structure side
The other direction is the application structure side.
A vertical slice can start simple. But sometimes we notice a different kind of pressure:
Presentation logic leaks into business decisions.
Persistence details shape the use case.
External APIs are called directly from the core flow.
Testing requires too much infrastructure setup.
The same use case should be triggered from multiple places.
Or the application service becomes a broad god class with too many unrelated responsibilities.
At that point, the first escalation may simply be clearer concern separation:
Presentation.
Business: Application & Domain.
Infrastructure.

This alone can already improve a lot:
The controller handles input and output.
The application layer orchestrates the use case.
The domain layer protects business behavior.
The infrastructure layer deals with databases, external systems, messaging, and frameworks.
This is not yet Hexagonal or Clean Architecture. It is just basic concern separation. And often, that is already enough.
Later, if infrastructure coupling becomes a real concern, Hexagonal or Onion Architecture may pay off: Ports can protect the application or domain from databases, frameworks, messaging, or external systems. Adapters can keep technical details outside the core.
But I would still be careful before introducing ports everywhere:
A port that protects a real boundary is useful.
A port with only one implementation may just be ceremony.
If use cases themselves become important architectural elements, Clean Architecture may become useful as use cases are a first class citizen in this pattern, especially when explicit input and output boundaries matter and business rules need strong protection from frameworks and infrastructure.
But again, this structure should earn its place. For a simple CRUD feature, full Clean Architecture can easily be more noise than signal.
So on the application side, the escalation may look like this:
Vertical slice
→ clearer layers
→ Hexagonal / Onion boundaries
→ Clean Architecture when explicit use cases and protected rules justify itThe important questions are:
Do we need better boundaries around the application?
Or are we only adding structure because the template says so?
These paths are independent
What I like about this view is that the two paths are independent:
You can have a vertical slice with almost no domain model.
You can have a vertical slice with a rich domain model.
You can have layered architecture with an anemic model.
You can have Hexagonal Architecture around procedural application services.
You can have Clean Architecture with a rich DDD-style entity layer.
The patterns do not automatically solve the same problem. Vertical Slices mainly help with feature locality. Layers help with concern separation. Hexagonal and Onion Architecture help with dependency direction and infrastructure boundaries. Clean Architecture on top makes use cases and boundaries more explicit. A rich domain model protects business behaviour. Additionally, CQRS can help when reads and writes have different requirements. Domain events help when meaningful facts should trigger decoupled reactions.
These are different forces. So before choosing a pattern, I like to ask:
What kind of pressure are we actually seeing?
Is the feature hard to find?
Are concerns mixed?
Are business rules duplicated?
Are invalid states too easy to create?
Is infrastructure leaking into the core?
Do multiple adapters need the same use case?
Are reads and writes diverging?
Do meaningful business facts need independent reactions?
Depending on the answer, the next step may be completely different.
Don’t escalate everywhere
The point is not to walk the full escalation path for every feature. Quite the opposite. Some code is mostly glue code:
receive data > map it > call another system > store something > return a response.
For this kind of code, too much architecture can make things much harder to understand.
Other code contains important business decisions:
It protects invariants.
It controls lifecycle transitions.
It represents concepts that matter to the business.
That code usually deserves more care.
The mistake is treating both kinds of code the same:
Glue code gets over-engineered.
Core domain logic stays hidden in long services or handlers.
Good architecture starts with noticing the difference.
A small example
Imagine a membership management system of a gym. A first feature could be ActivateMembership.
At the beginning, this may be a simple vertical slice:
features/
activateMembership/
ActivateMembershipController
ActivateMembershipRequest
ActivateMembershipHandler
ActivateMembershipResultThe handler creates the membership, stores it, maybe creates an invoice, and returns the result. For this initial feature, that may be enough.
Later, another use case is introduced: PaymentReceived. Now the system has to decide what happens when a payment arrives:
Was the membership suspended for non-payment?
Is the membership still within the valid period?
Was the payment already processed?
Should the membership be reactivated?
These are no longer only technical workflow steps but actual business decisions.
So maybe the domain model starts to grow:
Membership
MembershipStatus
BillingReferenceAnd instead of changing fields in the outside application function, we introduce methods like:
billingReference.markPaid(...)
membership.reactivateAfterPayment(...)Later, the billing provider becomes more complex:
Maybe we want to isolate the external API.
Maybe we want to test the use case without the provider SDK.
Then a focused outbound port may make sense:
CreateMembershipInvoiceor, for the payment flow:
FindPaymentByReferenceThe point is that the port should describe what the application needs, not expose everything the external provider can do.
A broad BillingProviderPort can easily become a second provider API inside our application.
A focused port keeps the boundary smaller:
CreateMembershipInvoice
createInvoiceForMembership(...)
FindPaymentByReference
findByReference(...)Even later, the same use case may be triggered by REST, a message listener, and so on. At that point, making the use case boundary explicit may be useful, which is where Clean Architecture would come into play.
Architecture as escalation
This is why I increasingly think about architecture as escalation:
Start with something understandable.
Keep the use case visible.
Then let the design grow where it needs to grow.
On the domain side, grow from simple logic toward a richer model when business rules and invariants deserve it.
On the application side, grow from vertical slices toward layers, ports, adapters, or Clean Architecture when boundaries, dependency direction, and use-case clarity deserve it.
Architecture is not about applying the most complete pattern. It is about introducing the right structure at the right time.
Enough structure to keep the code understandable, testable, and evolvable, but not so much structure that every small feature turns into a ceremony.
AI agents need the same judgment
This also becomes important with AI-assisted development. AI agents are very good at generating structure.
Sometimes too good.
If we ask an agent to “implement this with good architecture”, it may create an impressive folder structure. But impressive is not the same as appropriate:
It may apply Clean Architecture to simple CRUD.
It may introduce interfaces without a real dependency boundary.
It may add events where a direct call would be clearer.
Or it may keep everything procedural even when the feature clearly contains important business rules.
So I think that an AI agent can also profit from such an escalation model. It should not only know architecture patterns but also know when not to use them (yet).
That is the idea behind our free Modern Architecture Design Patterns AI Agent Skill.
The skill helps an AI coding agent start with a vertical slice by default and then decide whether the feature should stay simple or evolve toward a small domain model, richer DDD-style modeling, Layered Architecture, Onion Architecture, Hexagonal Architecture, Clean Architecture, CQRS, or domain events.
The goal is not:
“Generate the Clean Architecture folders.”
The goal is:
“Reason about the feature and escalate only where it pays off.”
You can download it here:
codeartify.com/downloads#modern-architecture-design-patterns-skill
We also cover the same thinking in our Application Architecture Patterns Workshop, where we look at internal application structure, trade-offs, concern separation, vertical slices, ports and adapters, domain modeling, CQRS, and events in a practical way.


