Skip to content

Tactical Frontend Design

Architecture eventually has to become concrete.

Strategic decisions may define the domain boundaries of a system, who owns which responsibilities, and which parts should evolve independently. At some point, however, those decisions have to turn into code. The question is no longer only where a boundary should be, but how that boundary becomes visible in the project.

Which file loads data? Where is a ViewModel created? Who starts a command? Where do mapping, navigation, and notifications belong? And which of those responsibilities should never have ended up in a component in the first place?

That is where Tactical Frontend Design begins.

The term is deliberately inspired by Domain-Driven Design.

DDD distinguishes between strategic decisions about domains, boundaries, and collaboration, and tactical design within those boundaries. Tactical design gives domain concepts concrete forms in code, such as entities, value objects, aggregates, or repositories.

A frontend does not need to reproduce those concepts mechanically. An Angular project does not become better simply because every DDD term appears somewhere in the folder structure.

The underlying idea is still useful:

Architecture eventually has to take shape in code.

In a frontend, those shapes may look different. A read flow can organize a domain-oriented projection for the UI. A ViewModel can describe exactly what a view needs. A command can model a change together with its consequences. Mapping can protect the frontend from leaking external models through the application. Notifications, navigation, or invalidation can have an explicit place instead of appearing wherever an HTTP call happens to finish.

That is why I call this area tactical: it is about the concrete cut inside a domain boundary.

Many architecture problems do not begin because a team is missing the right vocabulary. They begin because responsibilities gradually move to whichever place is most convenient at the time.

A page initially only loads data. Then loading state is added. Then error handling. Then mapping, a toast, navigation after saving, a reload, and a couple of special cases. Eventually the component is orchestrating half a use case.

The same thing can happen to a store. What started as state management slowly takes over HTTP calls, mapping, commands, form logic, routing, and domain decisions.

That can work surprisingly well for quite a long time. Which is exactly why it is worth discussing the cut before the problems become expensive.

The examples in this section therefore do not show isolated patterns. They show small but complete code slices. The interesting question is not only whether a store, mapper, or command is useful, but how those pieces collaborate and where their responsibilities stop.

For individual concepts, see ViewModel Aggregation, Event-driven Projection, and the Anti-Corruption Layer.

Here, the next step is more concrete: the architectural idea becomes an executable slice.

The examples use a modern Angular stack with Angular 21, NgRx 21, Signals, and NgRx Signal Store. Those versions define the available toolbox, not the architecture.

A dedicated read store with its own mapper, ViewModel, and tests can be a good fit for a long-lived domain slice. In a small and stable back-office dialog, the same structure may simply be unnecessary overhead.

So these examples are not meant to say:

“This is the correct way to build it.”

They are meant to provide a concrete design that can be discussed.

Whether a cut makes sense depends on domain complexity, expected lifetime, rate of change, team experience, test strategy, and the architecture already in place.

Good architecture needs structure.

But structure by itself is not a quality attribute.

The first examples use familiar CRUD flows because they make it easy to see how differently reading and writing can behave in a frontend.

Retrieve Slice: Loading Data Without Component Logic follows a read flow from infrastructure to the ViewModel.

Create Slice: Writing as a Command Flow treats creation as more than a POST: it is a change with a domain result and potentially observable consequences.

Update Slice: Form State Is Not a DTO looks at the boundary between edit state, backend models, and commands.

Delete Slice: Deletion Is a Domain Decision shows why even a seemingly simple delete flow can involve more than an HTTP request.

There are also reactions that reach beyond a single slice. Notification Flow, Routing After a Successful Command, and Reload and Invalidation After Success explore where those reactions belong without pushing them back into the component that happened to trigger the command.

Tactical Frontend Design sits somewhere between two extremes.

On one side are architectural statements such as “we need clear boundaries.” That may be correct, but it only gets us so far when nobody knows which responsibility should live in which file.

On the other side are technical statements such as “we use NgRx Signal Store with withMethods and withHooks.” That may also be correct, but it still says nothing about what the store should actually own.

This section tries to stay in the middle: concrete enough to build from, but abstract enough to understand why the cut was chosen.

The examples are not final answers. They are designs that are concrete enough to be tested, criticized, and changed in a real project.