Skip to content

Why Layering?

An application can be built without consciously thinking about layering. Components can load data, services can hold state, stores can make HTTP requests, and somewhere in between, the business logic finds a place to live.

That works.

At least for a while.

As an application grows, however, another question becomes increasingly important: Where does a particular responsibility actually belong?

That is where layering begins.

Separating an application into distinct areas of responsibility is one of the established ideas of software architecture.

Martin Fowler’s Presentation Domain Data Layering, for example, separates presentation, domain logic, and data access. One of its major benefits is that developers can focus on a limited part of the system at a time: someone working on business logic should not also have to think about the details of the user interface or how data is retrieved.

Domain-Driven Design uses concepts such as User Interface, Application, Domain, and Infrastructure – Eric Evans describes these layers in his DDD Reference. Other architectural approaches use different representations. Alistair Cockburn’s Hexagonal Architecture, for example, is less concerned with layers above and below one another and more with inside and outside: domain logic should not depend on whether it is accessed through a graphical user interface, a test, or another technical adapter. Nor should it be coupled to a particular database or external service.

The names, boundaries, and diagrams differ. The underlying idea remains similar:

Different responsibilities should not be allowed to bleed into each other without control.

An important influence on how I think about frontend layering has been Manfred Steyer’s work on DDD-inspired Angular and frontend architectures.

The structure used in this series does not adopt those approaches unchanged. It combines that inspiration with lessons from real projects, long-term maintenance, and collaboration within development teams.

In some areas, my interpretation deliberately differs from more traditional DDD models. This is particularly visible in +state: in this model, it carries a large part of the domain logic. Business rules, domain-derived state, and state transitions live here. The Application layer, by contrast, deliberately remains thin and close to the view.

The following articles therefore do not attempt to define the correct DDD layering model for frontend applications. They describe a DDD-inspired approach that has worked well in practice and, above all, aims to answer one everyday question:

Which responsibility belongs where?

Layering often first becomes visible as a directory structure. Many developers will recognize structures such as this from traditional backend applications:

src/
├── controllers/
├── services/
├── repositories/
└── entities/

Controllers receive requests, services coordinate application logic, and repositories handle persistence. Different responsibilities are initially given different places to live.

Such a structure is useful. But it is not architecture by itself.

Directories merely show which areas of responsibility are intended. Whether those boundaries become a sustainable architecture is decided in the code.

For every area, it must be clear which responsibilities belong there and where those responsibilities end. Only when a team understands and consistently respects these boundaries does a directory structure become an architecture.

Good architecture does not eliminate every discussion. It should, however, prevent teams from having the same fundamental discussions again for every feature.

Where does a business rule belong?

Who is allowed to make HTTP calls?

Where is a ViewModel created?

Should a component access the store directly?

Who aggregates multiple domain-related data streams?

Where does derived state belong?

Without shared rules, these questions are answered differently from developer to developer and from feature to feature. Architecture then emerges gradually from a collection of local decisions.

Clear layer boundaries provide guardrails. Not every decision has to be made again.

This reduces recurring discussions and makes a codebase more predictable. When opening an unfamiliar feature, developers should already have a reasonable idea of where a certain kind of logic is likely to be found before opening the first file.

This effect is often underestimated.

If I am working on the Presentation and know that no business rules are implemented there, I do not need to understand large parts of the application at that moment.

If I change a domain rule, I do not want to think about which UI framework currently renders it.

If I integrate data from a backend, I should not have to care which button will eventually modify it.

Layering reduces the amount of context we need to keep in our heads for a particular change. That helps not only during implementation, but also when debugging, reviewing code, onboarding developers, or refactoring.

If responsibilities have clear homes, developers need to understand less of the overall system in order to make a local change safely.

That applies to humans – and increasingly to tools as well.

With AI-assisted development, this property gains additional importance.

An agent can generate code very quickly. What it does not automatically know are the architectural decisions of a particular project. Without clear boundaries, many approaches initially appear plausible:

HTTP inside the store?
Business rule inside the component?
Mapping inside the facade?
Navigation from state?

All of these can be implemented technically. What matters is which one belongs in the architecture of this application.

A clearly defined architecture reduces that solution space.

If business rules belong in +state, Infrastructure encapsulates the technical outside world, and Presentation remains free of domain logic, these rules can be communicated not only to developers.

They can also be expressed through architecture documentation, agent instructions, generators, dependency constraints, reviews, and static checks.

Architecture no longer has to be renegotiated for every generated piece of code.

An agent does not receive only the task:

Implement this feature.

It also receives the guardrails:

Implement this feature within this architecture.

The faster code can be produced, the more important it becomes to define the rules that constrain its structure.

Clear boundaries are useful for more than keeping a codebase tidy. They create options.

If business logic does not live inside components, the same domain logic can be exposed through different user interfaces. An Angular Material presentation, an Ionic presentation, or another UI does not have to reimplement the same domain decisions.

This is not merely a theoretical scenario. In one of my long-running projects, Angular Material and Ionic were maintained in parallel on top of the same domain foundation for an extended period of time.

That worked because the boundary was consistently respected: the Presentation was responsible for displaying domain information and receiving user interactions, but it was not allowed to develop a second version of the business logic.

Replaceability is not necessarily the primary goal.

An architecture should not become more complex simply because a UI framework might theoretically be replaced one day.

Instead, the ability to replace one part can be a useful test of the quality of a boundary:

How much would have to change if only the presentation or only a technical integration changed?

The more unrelated domain code is affected, the weaker that boundary probably was.

The same applies to technical communication. If Infrastructure is properly encapsulated, the domain does not need to know whether data arrives through HTTP, local storage, or another mechanism.

If domain state has a clear home, derived state and business rules do not have to be scattered across components, services, and views.

The following articles use a deliberately frontend-oriented model that differs from classical DDD layering.

A feature might be organized like this:

my-feature/
├── domain/
│ ├── infrastructure/
│ ├── +state/
│ └── application/
├── models/
└── presentation/

Here, domain is an organizational umbrella for the non-visual core of the feature. It should not be confused with the classical Domain Layer from DDD.

The directory structure itself should also not be mistaken for the direction in which data flows.

For the following articles, we will follow the boundaries that data crosses on its way from the technical outside world to the user interface:

Technical outside world
Infrastructure
+State
Application
Presentation
User

Each area has a clearly defined responsibility.

Infrastructure encapsulates the technical outside world and translates technical communication into a form the feature can work with.

+State represents the domain’s runtime state. Business rules, state transitions, domain-derived state, and selections live here.

Application deliberately remains thin. For each view, a dedicated facade collects existing data flows and exposes them as a stable, view-specific contract to the Presentation.

Presentation is responsible for rendering and user interaction without reinterpreting domain rules.

models plays a special role. Some models deliberately cross layer boundaries. A ViewModel, for example, may be produced by the Application and consumed by the Presentation without belonging entirely to either of them.

This model is not a universal truth, nor is it an attempt to transfer Domain-Driven Design unchanged into the frontend.

It is my interpretation of frontend layering, inspired in part by Manfred Steyer’s work and extended through experience from real projects and long-term use.

Above all, it provides guardrails for where a responsibility begins – and where it ends.