Skip to content

Tactical Frontend Design

Frontend architecture rarely fails because nobody knows the right terms.

ViewModel. Store. Mapper. Command. Query. Anti-Corruption Layer. Feature Boundary. Event.

The terms are quickly explained.

The harder question comes next:

Where does the code actually go?

Not in the architecture diagram. Not in the meeting room. Not in the blog post.

But concretely, in the project:

  • Which file loads data?
  • Which file maps DTOs?
  • Which file knows the ViewModel?
  • Which file triggers a command?
  • Which file shows a toast?
  • Which file decides on navigation?
  • Which file is allowed to reload a store?
  • And what must never end up in the component?

This section is called Tactical Frontend Design because it starts exactly there.

Not with the big architectural vision.

But with the design in the code.


This section doesn’t explain, once again, why ViewModels, stores, Anti-Corruption Layers, or events can be useful.

There are already foundational articles for that:

  • ViewModel Aggregation describes why a ViewModel is a deliberate UI decision, not a renamed backend DTO.
  • Event-driven Projection describes why UI state can be understood as a projection from events, not as a freely mutable data bag.
  • Anti-Corruption Layer describes why foreign models shouldn’t travel through the frontend unfiltered.

This section does something different.

It takes those ideas and asks:

What does this look like as a runnable code slice?

Patterns describe the idea.

Tactical Frontend Design shows the design.


In many frontends, everything starts out harmless.

A page component loads data.

Then loading state gets added.

Then error handling.

Then a mapper.

Then a toast.

Then navigation after saving.

Then a reload of the list.

Then a special case.

Then a second special case.

At some point, the component is no longer a UI building block, but a small use-case orchestrator with a template.

Or the store becomes a catch-all for everything:

  • HTTP calls
  • mapping
  • commands
  • query state
  • form state
  • notifications
  • routing
  • reload logic
  • domain decisions
  • technical side effects

That works.

Until it doesn’t.

Tactical Frontend Design tries to make that responsibility visible again.

Not through more abstraction.

Through more deliberate boundaries.


The term is deliberately inspired by Domain-Driven Design.

In classic DDD, tactical design is about concrete building blocks like entities, value objects, aggregates, repositories, or domain services.

In the frontend, things are different.

An Angular frontend is not a small copy of the backend. It’s also not a place where you mechanically rebuild DDD terms just to make the folder tree look more domain-driven.

Still, the underlying idea holds:

Domain meaning needs shape in code.

For this section, that means:

  • A read flow isn’t just a GET.
  • A create flow isn’t just a POST.
  • A ViewModel isn’t just the DTO with a different name.
  • A command isn’t just a store method with an HTTP call.
  • A toast isn’t a detail that happens to appear somewhere after saving.
  • Routing isn’t a side effect that has to hide inside a button click handler.

The examples are conceptually oriented around DDD in the frontend, among other things drawing on approaches Manfred Steyer has shaped in the Angular ecosystem.

But they don’t adopt these approaches as dogma.

They use them as a toolbox.


The code slices in this section are built around a modern Angular stack with Angular 21 and NgRx 21.

That’s not an arbitrary version number.

This section isn’t meant to show how to somehow keep legacy Angular applications running. It’s meant to show how to organise domain frontend flows with today’s tools:

  • standalone APIs
  • signals
  • NgRx Signal Store
  • modern NgRx patterns
  • functional providers, guards, and resolvers where they fit
  • modern control-flow syntax
  • clear feature boundaries
  • explicit ViewModels
  • separated read and command flows

Still, this isn’t about version marketing.

Angular 21 and NgRx 21 set the toolbox.

The real question stays architectural:

Where does responsibility live?


No design fits everywhere.

A dedicated read store with its own mapper, ViewModel, and tests can make a lot of sense when domain read projections are complex, several components consume the same state, or the slice will keep growing over time.

In a small, stable back-office dialog, the same design can be overengineering.

This section therefore doesn’t claim:

“This is the right way to do it.”

Instead:

“This is one way to design it — and now we can talk about it concretely.”

What’s always relevant:

  • team size and experience
  • domain complexity
  • expected lifespan
  • rate of change
  • test strategy
  • existing architecture
  • cost of the extra structure

Architecture isn’t an end in itself.

But missing structure isn’t simplicity either.


CRUD sounds technical.

In the frontend, though, these are usually domain flows with state, decisions, and side effects.

Some reactions don’t belong in the component that contains the button.

A successful command can trigger notifications, start navigation, or invalidate read stores. The question isn’t just that this happens, but where this reaction is modelled.

For a design to work across a team, it needs to be recognisable.

That’s why this also covers structure, names, and tests.


Frontend architecture often becomes useless at two extremes.

Too abstract:

“We need clear boundaries.”

True.

But which file do I move where?

Too technical:

“We use NgRx Signal Store with withMethods and withHooks.”

Good.

But what responsibility does this store get?

This section looks for the middle ground.

Concrete enough to create files.

Abstract enough to understand the design.

Open to criticism enough to be adapted in a real project.