Frontend Architecture and the Illusion of Boundaries

Introduction

Recently I reviewed a proposal in our Angular codebase introducing domain-based folders and facades between domains.

On the surface, it makes sense:

  • split code into domains

  • avoid cross-domain imports

  • expose public APIs

  • use facades for communication

But it raised a deeper question:

Are we actually reducing complexity, or just adding structure to manage complexity created by our architecture?


Frontend vs Backend: Different Nature of Problems

Backend systems naturally fit Domain-Driven Design because they deal with:

  • business consistency

  • transactional boundaries

  • long-lived domain rules

  • data ownership

Frontend systems are fundamentally different. They are:

  • user-flow driven

  • interaction-driven

  • UI composition driven

  • rapidly changing with UX

A single user journey often spans multiple backend domains:

  • account

  • payments

  • address

  • notifications

But to the user, it is still one flow.

This difference matters more than we usually admit.


A Concrete Example: Cross-Domain Flow in Angular

Take a simple flow:

A user applies for a new card and updates their mailing address during the process.

In a domain-split Angular architecture, this is typically divided into:

  • Card domain

  • Customer domain

Now Card needs to trigger an update in Customer.

Because direct access is discouraged, we introduce:

  • a Customer facade

  • an interface contract

  • an implementation hidden behind store/effects/services

So the flow becomes:

1
2
3
4
Card Domain
-> Customer Facade Interface
-> Customer Facade Implementation
-> State / Effects / API

A simple user intent becomes a multi-layered coordination chain.


Why This Feels Strange in React

To React developers, this often feels unnecessary.

Not because cross-feature dependencies don’t exist — they absolutely do — but because React tends to handle them differently.

Instead of introducing:

  • domain facades

  • interface contracts

  • global orchestration layers

React relies more on:

  • component composition

  • local state ownership

  • direct capability usage through hooks

Dependencies are handled through composition, not architectural indirection.


The Real Question

This leads to a more important question:

If the dependency already exists in the user flow, what do we actually gain by wrapping it in facades and domain boundaries?

We are not removing coupling.

We are:

  • formalizing it

  • layering it

  • redistributing it across abstractions

So the real question becomes:

If coupling is unavoidable in frontend flows, should we organize the system around domains at all — or around user flows instead?


The Microservice Parallel

This feels very similar to microservices.

Many systems moved from monoliths to microservices believing:

the monolith is the problem

But often the real issue was:

  • poor modularization

  • unclear boundaries

  • tight coupling

So complexity was not reduced — it was moved across boundaries.

Frontend architecture can fall into the same pattern:

  • domains

  • facades

  • interfaces

  • orchestration layers

We may end up managing complexity instead of reducing it.


Conclusion: Boundaries Should Follow Workflows

I still believe modularization matters.

But frontend systems are not primarily domain systems — they are workflow systems.

They optimize for:

  • interaction, not consistency

  • composition, not strict boundaries

  • user flows, not domain purity

In many cases, workflow or feature-level boundaries are more natural than backend-style domain boundaries.

Good frontend architecture should reduce coordination cost and improve composability — not introduce layers that only exist to manage existing layers.

Command vs Event: It’s About Where the Logic Lives

In most cases, the decision between sync and async is straightforward.

If you must know the result immediately (e.g. payment authorization, OTP), you go synchronous.

Otherwise, you go async.

The real question is:

When using async, should we send a Command or publish an Event?

1. Async Decision: Command vs. Event

Once we choose async, we usually end up with two options:

  • Send a Command

  • Publish an Event

At a glance, they look very similar — both go through queues.

But in practice, they lead to very different systems.

A simple comparison

** 2. The Real Problem in Practice**

In real systems, this decision is rarely clean.

You often see a lot of async commands already in place, and later a push to “move to events” for better decoupling. But after the change, things don’t necessarily get simpler — just different.

Logic doesn’t disappear — it moves

What do you feel

Command:

  • Easy to follow — everything is in one place

  • Low coordination cost

  • But tends to leak logic and create coupling

Event:

  • Decoupled and extensible

  • But spreads logic across services

  • And increases coordination and debugging cost

In short:

  • Command keeps things visible but coupled

  • Event decouples but spreads the logic

3. What should drive the decision?

The real decision is not “Command or Event” in isolation.

It is a trade-off between:

  • keeping the logic centralized, at the cost of tighter coupling

  • decoupling services, at the cost of distributing the logic

So the useful question is:

What matters more in this case: visibility or decoupling?

A few factors usually drive that decision:

  • Flow visibility — Do we need one place to understand and debug the business flow?

  • Expected growth — Is this likely to attract more downstream reactions over time?

  • Coordination cost — Can the teams afford the extra schema, tracing, and ownership overhead?

  • Cost of coupling — Is the downstream a simple utility, or a separate domain we don’t want to bind to?

In practice:

  • Choose Command when keeping the flow visible and local matters more

  • Choose Event when reducing coupling and enabling future extensibility matters more