Skip to content

+State – where domain logic lives

The Infrastructure article turned a raw "ACTIVE" into our own CustomerStatus.Active.

And that is as far as it went.

We still do not know whether this customer is allowed to place an order. We do not know which actions are currently available, which state transition is valid, or which selection matters for a particular domain operation.

This is where +State begins.

Infrastructure translates the outside world into a controlled representation of our own. +State relates those values to each other and interprets them in the context of our domain.

At first glance, a +State often looks harmless:

type CustomerState = {
customers: Customer[];
selectedCustomerId?: CustomerId;
loading: boolean;
};

On its own, this state merely contains values. It becomes interesting through the questions we can answer with those values:

Which customer is selected?
Is the selection still valid?
Which customers match a particular condition?
Which actions are currently allowed?
What follows from a state transition?

Not every one of these questions is automatically a business rule. Nor is every field in a store necessarily domain state. A loading flag, for example, remains technical runtime state – even though, as the note Loading Isn’t a Boolean shows, it is rarely a simple switch.

The important difference is that +State does not stop at storing values. This is where state transitions, derived state, and domain decisions emerge from the current state of the application.

State gains meaning through its relationships, rules, and derivations.

+State combines state with business rules, derivations, and state transitions to produce domain-relevant results.

A very simple interpretation of state management looks like this:

API
Store caches response
Component reads response

That can be perfectly sufficient. Architecturally, however, the store in this model is little more than a cache for backend data. It keeps values around without giving them any meaning of its own.

Our understanding of +State goes further:

Infrastructure
+State
├── owns state
├── processes domain intents
├── applies business rules
├── models state transitions
└── produces derived state

Whether this is implemented with Signals, NgRx, Redux, Zustand, RxJS, or another state-management approach does not change that responsibility.

The library decides how state is managed. The architecture decides which responsibilities that state carries.

Infrastructure translated:

"ACTIVE"
CustomerStatus.Active

+State can now evaluate that state together with additional information:

CustomerStatus.Active
+
CreditHold.None
+
Account.enabled
canOrder

Whether a customer is allowed to place an order is no longer a technical translation. It is a rule of our system.

The same applies to questions such as:

  • Can an order be cancelled?
  • Can a record be deleted?
  • Is this state transition valid?
  • Which action is available in the current domain situation?
  • Does an entity satisfy a particular domain condition?

A business rule does not become presentation logic just because its result eventually disables a button.

And yet this is precisely where domain rules often creep into the UI:

disabled = customer.status !== CustomerStatus.Active || customer.creditHold !== CreditHold.None;

As long as there is only one user interface, this can appear harmless.

Once the same decision is needed in an Ionic presentation, a mobile interface, or somewhere else, multiple implementations of the same rule exist.

The domain state can expose a single statement instead:

canOrder;

The Presentation then decides how to represent that state: as a disabled button, a greyed-out action, or a menu item that is not shown at all.

It does not decide again whether the action is allowed.

Not every piece of information has to be stored separately.

From:

customers
+
selectedCustomerId
selectedCustomer

we can derive selectedCustomer.

The same applies, for example, to:

customers
+
filter
+
sort
visibleCustomers

selectedCustomer and visibleCustomers do not have to be stored as additional copies of their source state and then manually synchronized after every change.

They can emerge from the state that already exists.

This avoids duplicate sources of truth and the synchronization problems that come with them. As soon as two stored values represent the same information, something has to keep both permanently aligned. A derived value does not require that synchronization.

Values such as selectedCustomer, visibleCustomers, and canOrder are derived from source state such as customers, selection, filter, and sorting.

The article Reactive filtering and sorting explores the distinction between query state and derived state in more detail, including why filters and sorting can themselves become part of the state.

A selection does not require storing an entire entity a second time either.

entities
+
selectedId
selectedEntity

If the selection matters to the domain or to application-wide behavior, the ID may be sufficient as source state. The selected entity can then be derived from it.

Whether a selection belongs in +State at all depends on what it means. A table row that is highlighted for a moment can be pure Presentation state. A selection that drives subsequent domain operations has a different lifetime and responsibility.

Select by ID shows how such a selection flow can be structured in practice.

Derived state does not have to come from a single source.

articles
+
authors
+
categories
projection

Several state sources can together form a consumable projection.

What matters is less how many stores or sources are involved and more where responsibility for the resulting information belongs.

ViewModel from multiple data sources shows how several reactive sources can work together. Event-driven Projection covers projections that are built and updated through events.

Both illustrate the same underlying idea: consumers do not necessarily need to know the raw structure of the state underneath.

None of this implies that a feature should have exactly one large +State.

Store boundaries are not primarily determined by file size or by several states happening to use the same entity type.

More useful questions are:

  • Which responsibilities belong together?
  • Do these states have the same lifecycle?
  • Do they change for the same reasons?
  • Who owns them?
  • Which consumers use them?

Two states can both work with Customer and still have completely different responsibilities.

Conversely, several data types can together form one coherent domain state.

When the read store gets too large discusses these boundaries in detail.

For +State, the important point is this: the layer describes a kind of responsibility, not a requirement to put everything into one store.

+State does not only own readable state. This is also where domain-level change requests meet the current state.

deleteRequested
current state
allowed / rejected
state change

Or:

completeOrder
Business Rule
State Transition

An intent initially describes an intention.

“This customer should be deleted” is different from “This customer has been deleted.”

Whether the requested change is allowed, and which domain state results from it, depends on the rules and the current state.

The CRUD Slices series describes in detail how Create, Retrieve, Update, and Delete flows can be structured around those responsibilities.

The important boundary here is simpler: a domain-level change request should not first acquire its meaning inside a component or an HTTP service.

+State can provide state and derived values that later become part of a ViewModel.

That does not mean every view-specific projection has to be fully assembled inside the store.

A projection can make domain state consumable. The Application layer can then combine several such pieces of information for a concrete view or expose a suitably shaped API.

Where exactly that boundary belongs depends on the particular slice.

ViewModel Aggregation discusses in more detail how ViewModels can be composed from several data sources without leaking backend structures or internal state details into the Presentation.

For +State, the important point is that business rules and domain-relevant derivations should not first appear while building a specific user interface.

+State can and should, for example:

  • own domain-relevant runtime state
  • apply business rules
  • model domain state transitions
  • process domain intents
  • produce derived state
  • expose relevant selections
  • produce domain filters and projections
  • react to domain-relevant events
  • centralize rules that apply independently of the concrete Presentation

+State should not:

  • implement HTTP requests itself
  • parse DTOs
  • let foreign API contracts leak into domain logic
  • use HttpClient, fetch, or SDK-specific APIs
  • interpret technical API error codes
  • control the router directly
  • open dialogs
  • show snackbars or toasts
  • know DOM or UI-framework details
  • distinguish between Angular Material, Ionic, or Bootstrap
  • perform concrete view orchestration that belongs in the Application layer

For edge cases, one simple question is surprisingly useful:

Would this rule still apply if Material were replaced by Ionic tomorrow?

If the answer is yes, it is a good candidate for +State.

That is not a definition, but it is a useful rule of thumb.

The advantages of this boundary become particularly visible when several user interfaces share the same domain logic.

In one long-running project, Angular Material and Ionic were used in parallel for years on top of the same domain foundation.

That worked because business rules were not implemented separately in both Presentations.

+State
/ \
/ \
Material Ionic

The Presentation decides how something is rendered. +State provides the domain decisions.

If canOrder had been calculated separately in both Presentations, two implementations would have had to remain synchronized indefinitely. Sooner or later, the two interfaces could have produced different answers to the same domain question.

A shared +State layer provides the same domain rules and derived state to both an Angular Material and an Ionic Presentation.

The same separation creates several other benefits.

Business rules become easier to find. A domain decision spread across components, pipes, form validators, services, and templates can no longer be understood in one place. It has to be reconstructed.

If you know that canOrder originates in the domain state, you also know where to look for the rule.

Derived state remains consistent because multiple copies of the same information do not have to be synchronized manually.

And business rules can be tested independently of a concrete user interface. Whether Material renders a button or Ionic renders a menu item is irrelevant to the canOrder decision.

These advantages do not come from the state-management library.

They come from the boundary.

Giving domain logic a clear home does not mean moving every variable into a store.

Values such as:

isDialogOpen
hoveredRow
activeTab
tooltipVisible
accordionExpanded

are often Presentation state. They describe how a particular interface is currently being displayed.

Even this boundary is not always absolute. An activeTab, for example, might eventually represent a domain-relevant selection. The variable name tells us very little by itself.

A more useful question is:

Does this state have meaning independently of how it is currently presented?

If the answer is no, +State is probably not its natural home.

+State owns domain-relevant state, business rules, derivations, selections, state transitions, and domain intents.

A concrete view still should not have to know the entire state.

A customer-list view might only need:

customers
loading
selectCustomer()
createCustomer()

A detail view needs a different slice of the same domain logic.

This creates the next boundary of responsibility: +State owns and interprets the domain. Application shapes an API from that domain for a concrete use case or view.

The domain logic remains in one place without forcing every Presentation to understand its entire internal structure.