Skip to content

Latest commit

 

History

History
1153 lines (872 loc) · 31.4 KB

File metadata and controls

1153 lines (872 loc) · 31.4 KB

Architecture

Clean Reactive Architecture is an implementation of the Clean Architecture concept for reactive applications, built around the observer pattern.

The Clean Architecture concept is outlined with the following circle diagram:

clean-architecture

The implementation of the concept for reactive applications is outlined with the following UML diagram:

clean-reactive-architecture

The double lines on the diagram represent boundaries, which data crosses as primitive data types or data structures, for example DTOs or plain objects.

mermaid
graph TD

subgraph B1["Boundary"]
  G["Gateway"]
  ER["External Resource"]
end

subgraph B2["Boundary"]
  UI["User Interface"]
end

subgraph B3["Boundary"]
  E["Entities"]
end

P["Presenter"]
C["Controller"]
PI["Presenter < I >"]
CI["Controller < I >"]
GI["Gateway < I >"]
UC["Use Case Interactor"]

%% implementation relation
P -. implements .-> PI
C -. implements .-> CI
G -. implements .-> GI

%% dependency relation
UI -- depends --> PI
UI -- depends --> CI
C -- depends --> UC
P -- depends --> E
UC -- depends --> E
UC -- depends --> GI
G -- depends --> ER

classDef boundary fill:none,stroke:#666,stroke-width:2px,stroke-dasharray: 5 5;
class B1,B2,B3 boundary;
Loading

Definition of units

  • Entities: A unit that maintains one or more enterprise business entities, application business entities, or a mix of both.
  • Use Case Interactor: A unit that orchestrates the flow of data to the entities, and directs those entities to use their business rules to achieve the goals of the use case.
  • Gateway: A unit that encapsulates access to an external system or resource.
  • Controller: A unit that handles input data from the user interface and converts it into a format most convenient for the use cases and entities.
  • Presenter: A unit that converts data from entities into a format most convenient for the user interface.
  • User Interface: A unit that displays information to the user based on the data prepared by the presenter, and captures user input and transfers it to the controller.
  • External system or resource: A unit that represents external systems or resources the application interacts with, e.g., services, databases, storage, or other applications and libraries with an API.

Definition of concepts utilized by the units

  • Enterprise Business Entity: An entity that encapsulates enterprise business rules and data.
  • Enterprise Business Rules and Data: Rules and data that would exist even if the application didn't exist. These enterprise-wide rules are independent of any specific application.
  • Application Business Entity: An entity that encapsulates application-specific business rules and data.
  • Application Business Rules and Data: Rules and data specific to the application's functionality. These are application-wide rules that don't exist outside the context of a particular application.
  • State: The data of entities at a given point in time, typically represented as an object structure.
  • Valid State: One of a finite number of states considered valid according to the enterprise and application business rules.

NOTE: The Clean Architecture concept remains the primary source of truth. This document does not redefine its principles, instead, it provides concrete guidance for areas where the original concept is abstract or implementation-agnostic. Unless explicitly stated otherwise, engineers and developers should rely on the principles of Clean Architecture when making architectural decisions.

See also:

Robert C. Martin. The Clean Architecture

Robert C. Martin. (2017). Clean Architecture: A Craftsman's Guide to Software Structure and Design

Q&A

Where did the diagram come from?

Clean Architecture is a formalized architectural concept for application software. The most overlooked factor here is that the implementation of Clean Architecture is a UML diagram. The concept itself is too abstract, while the codebase is too concrete. And a UML diagram bridges that.

clean-architecture-implementation-flow

mermaid
flowchart TD

A["Clean Architecture Concept"] --> B["UML Diagram (units, boundaries, flow)"]
B --> C["Application Codebase (concrete implementation)"]
Loading

For a typical backend application written in Java, a well-known Clean Architecture implementation has existed for years.

clean-architecture-web-based-java-system

mermaid
graph TD

subgraph B1["Boundary"]
  C["Controller"]
  P["Presenter"]
  VM["View Model < DS >"]
end

ID["Input Data < DS >"]
IB["Input Boundary < I >"]

OD["Output Data < DS >"]
OB["Output Boundary < I >"]

subgraph B2["Boundary"]
  V["View"]
end

UC["Use Case Interactor"]

DAI["Data Access Interface < I >"]

subgraph B3["Boundary"]
  DA["Data Access"]
  DB["Database"]
end

subgraph B4["Boundary"]
  E["Entities"]
end

%% implementation relations
P -. implements .-> OB
UC -. implements .-> IB
DA -. implements .-> DAI

%% dependency relations
C -- depends --> IB
C -- depends --> ID

P -- depends --> VM
P -- depends --> OD

V -- depends --> VM

UC -- depends --> ID
UC -- depends --> OB
UC -- depends --> OD
UC -- depends --> DAI
UC -- depends --> E

DAI -- depends --> E
DA -- depends --> DB

classDef boundary fill:none,stroke:#666,stroke-width:2px,stroke-dasharray: 5 5;
class B1,B2,B3,B4 boundary;
Loading

Clean Architecture. A craftsman’s guide to software structure and design. Robert C. Martin. Copyright © 2018 Pearson Education, Inc.

However, this implementation does not directly apply to reactive applications. Attempts to use it introduce code solely for adapting it. Nevertheless, the Clean Architecture concept is universal and an implementation tailored for a reactive application, for example with external API integration, is constructed as follows:

clean-architecture-to-clean-reactive-architecture

mermaid
graph TD

subgraph CA["The Clean Architecture Circle Diagram"]

  subgraph L5["Frameworks & Drivers"]
    CUI["UI"]
    CWEB["Web"]
    CDB["Database"]
    CEXT["External Interfaces"]
    CDEV["Devices"]
  end

  subgraph L4["Interface Adapters"]
    CC["Controllers"]
    CG["Gateways"]
    CP["Presenters"]
  end

  subgraph L3["Application Business Rules"]
    CUC["Use Cases"]
  end

  subgraph L2["Enterprise Business Rules"]
    CE["Entities"]
  end

  %% Layer dependencies (inward direction)
  L5 -- depends --> L4
  L4 -- depends --> L3
  L3 -- depends --> L2

end

subgraph CRA["The Clean Reactive Architecture UML Diagram"]

  G["Gateway"]
  ER["External Resource"]

  UI["User Interface"]

  E["Entities"]

  P["Presenter"]
  C["Controller"]
  PI["Presenter < I >"]
  CI["Controller < I >"]
  GI["Gateway < I >"]
  UC["Use Case Interactor"]

  %% implementation relation
  P -. implements .-> PI
  C -. implements .-> CI
  G -. implements .-> GI

  %% dependency relation
  UI -- depends --> PI
  UI -- depends --> CI
  C -- depends --> UC
  P -- depends --> E
  UC -- depends --> E
  UC -- depends --> GI
  G -- depends --> ER

end

CA -. implementation .-> CRA
Loading

NOTE: The diagram shows units (conceptual responsibilities), not files or folder structure. Units are how the system should be thought about at runtime - which unit owns what, dependencies, where responsibilities lie. How units are organized into files is a separate organizational decision, independent of the architecture.

Where does the application business entity come from?

Clean Architecture distinguishes between enterprise business rules and data and application business rules. Enterprise rules and data are independent of any particular application and live inside entities. Application rules are specific to a particular application's behavior and live inside use cases and apply as orchestration logic during the use case call.

However, reactive client applications introduce a category of application-specific concern that requires an extension of this model: persistent, observable state with its own validity rules and lifetime, independent of any single use case invocation.

Consider the runtime state of an asynchronous refresh: idle, loading, error with a message. This state has its own valid configuration (transitions are not random - it cannot move directly from error to loading, it requires an explicit retry, an idle state cannot have an error message). The state has its own observable lifetime - different parts of the user interface may react to it independently. It makes sense only within applications that perform asynchronous synchronization. And it persists across many operations longer than any use case call.

This is application-specific, but it is not orchestration logic. It is state (data) with rules, which can be recognized as an application business entity. Clean Reactive Architecture puts it where it belongs - the entities layer, and gives presenters that read it and use cases that transition it.

Reactive client applications deal with this type of state constantly, though it rarely receives architectural recognition. Examples:

  • Operation stateidle / loading / error, per operation.
  • Initialization stateuninitialized / initializing / ready.
  • Session stateauthenticated / refreshing / expired.
  • Route state — current route, parameters, back stack.
  • Form state — field values, dirty flags, validation status, submission lifecycle.
  • Connectivity stateonline / offline / metered.
  • Selection state — multi-select sets, expansion flags, focus.

Each of these has its own validity rules and observable transitions. Each is application-specific.

Does it support unidirectional flow?

Clean Reactive Architecture supports unidirectional flow out of the box, and holds it more strictly than in architectures with stateful hubs. The flow never crosses and reverses.

Unidirectional flow of control and data is the following:

clean-reactive-architecture-flow-of-control

mermaid
graph TD

G["Gateway"]
GI["Gateway < I >"]
ER["External Resource"]
UI["User Interface"]
E["Entities"]
P["Presenter"]
C["Controller"]
PI["Presenter < I >"]
CI["Controller < I >"]
UC["Use Case Interactor"]

%% implementation relation
P -. implements .-> PI
C -. implements .-> CI
G -. implements .-> GI

%% dependency relation
UI -- depends --> PI
UI -- depends --> CI
C -- depends --> UC
P -- depends --> E
UC -- depends --> E
UC -- depends --> GI
G -- depends --> ER

%% flow
UI -. flow .-> CI
CI -. flow .-> C
C -. flow .-> UC
UC -. flow .-> E
E -. flow .-> P
P -. flow .-> PI
PI -. flow .-> UI
Loading

The controller receives user input, the presenter reacts and projects entities for the user interface. They both are never connected to each other - they share only the entities, where one path writes and the other reads. There is no single unit (hub) which sits on both paths.

Where do the SOLID principles fit?

For Clean Reactive Architecture SOLID was a generative lens, not a governing law. The UML diagram was derived by asking: "If we apply Clean Architecture to reactive apps, what units do we actually need, and how do they interact?" SOLID helped think through that derivation. But once the UML diagram existed, it became the primary specification. SOLID didn't need to be enforced anymore because the diagram's structure already encoded it. Some principles still guide development, but not as rules the codebase is audited against.

  1. The single-responsibility principle (SRP) - structural, not aspirational.

    Each unit of the architecture has its own and only one well-defined responsibility, and the diagram enforces it through reachability rather than through discipline. The presenter only reads entities and projects them for the user interface, the controller only converts user input into a use case call. The two are never connected, and no unit sits on both paths, so a unit cannot quietly acquire a second responsibility - there is no path along which it could.

    This is what the separation buys concretely: a decision on the write path can never be made from data prepared for display.

  2. The open–closed principle (OCP) - a completion milestone.

    In B. Meyer's original formulation a unit is closed once its public interface is declared. In the outside-in flow the user interface layout is built first, and presenter<I> and controller<I> are extracted from what that layout actually consumes. At this point the user interface unit is closed: everything downstream depends on the declared interfaces, not on the layout implementation. The unit can be handed off, and the remaining units can be built in parallel against the declared contract.

    At the same time it remains open for extension (composition): the same layout can be composed into other screens, or rendered against other implementations of its interfaces - mocks, fixtures, another driver - through the LSP.

    Practically, OCP is used here as a milestone in the development flow, a point at which a unit is done and work can move on. It is not a mandate to design extension points for hypothetical future requirements.

  3. Liskov substitution principle (LSP) - substitution as a working tool.

    Explicitly declared presenter<I>, controller<I> and gateway<I> interfaces make implementations behind them interchangeable for their consumers. For example, a local in-memory gateway and a remote one both satisfy the same gateway<I>, and neither the use case, nor the presenter, nor the user interface changes when one replaces the other.

    This is what makes it possible to develop and demo the application without a backend, to run integration tests that never touch the network, to vary behavior per environment, and to render the user interface against a mock presenter.

  4. Interface segregation principle (ISP) - a decomposition signal.

    The user interface unit is implemented top-down: one large layout that covers the feature's requirements is decomposed into smaller parts. The extracted interfaces support that decomposition. A thick presenter<I> is a signal that the user interface unit carries too much and should be split, and the segments of that interface suggest where the split may happen - each smaller part then gets its own, thinner interface. The same reading applies to a thick controller<I> or gateway<I>.

    ISP is applied here at refactoring time, on an interface extracted from a concrete consumer. It is not a reason to design small interfaces upfront.

  5. Dependency inversion principle (DIP) - the structural one.

    This is the principle the diagram actually rests on, rather than merely agrees with. Units from inner layers do not depend on concrete implementations of units from outer layers, they depend on abstractions. The use case unit (inner layer) depends on the gateway unit (outer layer) through the gateway<I> interface, and the user interface unit depends on presenter<I> and controller<I> rather than on their implementations. Remove this and the layers collapse into a graph of concrete imports - the boundaries on the diagram exist because of it.

See also:

B. Meyer. (1997). Object-Oriented Software Construction

Robert C. Martin. The Principles of OOD

How does it scale?

As the codebase grows, it will obviously need more units to share common logic. Following the Clean Architecture concept, the UML diagram can be extended with additional units, which will fit an additional circle (layer) in the circular diagram of the concept. Such extension is consistent.

The extended diagram is the following:

clean-reactive-architecture-extended

mermaid
graph TD

subgraph B1["Boundary"]
  G["Gateway"]
  ER["External Resource"]
end

subgraph B2["Boundary"]
  UI["User Interface"]
end

subgraph B3["Boundary"]
  E["Entities"]
end

P["Presenter"]
C["Controller"]
PI["Presenter < I >"]
CI["Controller < I >"]
GI["Gateway < I >"]
UC["Use Case Interactor"]
SE["Selector"]
TR["Transaction"]
EF["Effect"]

%% implementation relation
P -. implements .-> PI
C -. implements .-> CI
G -. implements .-> GI

%% dependency relation
UI -- depends --> PI
UI -- depends --> CI
C -- depends --> UC
P -- depends --> SE
UC -- depends --> SE
UC -- depends --> EF
UC -- depends --> TR
UC -- depends --> E
SE -- depends --> E
EF -- depends --> SE
EF -- depends --> E
EF -- depends --> GI
TR -- depends --> SE
TR -- depends --> E
G -- depends --> ER

classDef boundary fill:none,stroke:#666,stroke-width:2px,stroke-dasharray: 5 5;
class B1,B2,B3 boundary;
Loading

The diagram represents units which are empirically sufficient for a quite large codebase.

Definition of additional units

  • Selector: Unit that derives values or aggregates data structures from the entities without modifying them.
  • Transaction: Unit that transitions entities between two valid states, ensuring business rules are maintained.
  • Effect: Unit that manages data flows to, from, and across gateways (sequential, parallel, etc.) and derives data structures from them.
How does it share business logic?

Enterprise business rules and data are explicit in Clean Reactive Architecture, so they can be extracted into a separate core (library) to be shared across multiple reactive and non-reactive clients.

Such a core (library) will know nothing about any client. It will have its own API and, for example, its own mechanism for storing data. The most practial thing here is that the core (library) can be built following the same Clean Architecture concept but outlined with the request-response UML diagram - so one concept covers two different types of applications.

High level architecture:

shared-core-library

mermaid
flowchart LR

RC1["Reactive client"]
RC2["Reactive client"]
C["Core (library)"]

RC1 -- depends --> C
RC2 -- depends --> C
Loading

Architecture of the reactive client:

clean-reactive-architecture

mermaid
graph TD

subgraph B1["Boundary"]
  G["Gateway"]
  ER["External Resource"]
end

subgraph B2["Boundary"]
  UI["User Interface"]
end

subgraph B3["Boundary"]
  E["Entities"]
end

P["Presenter"]
C["Controller"]
PI["Presenter < I >"]
CI["Controller < I >"]
GI["Gateway < I >"]
UC["Use Case Interactor"]

%% implementation relation
P -. implements .-> PI
C -. implements .-> CI
G -. implements .-> GI

%% dependency relation
UI -- depends --> PI
UI -- depends --> CI
C -- depends --> UC
P -- depends --> E
UC -- depends --> E
UC -- depends --> GI
G -- depends --> ER

classDef boundary fill:none,stroke:#666,stroke-width:2px,stroke-dasharray: 5 5;
class B1,B2,B3 boundary;
Loading

Architecture of the core (library):

clean-architecture-request-response

mermaid
graph TD

subgraph B1["Boundary"]
  C["Controller"]
  P["Presenter"]
  VM["View Model < DS >"]
end

ID["Input Data < DS >"]
IB["Input Boundary < I >"]

OD["Output Data < DS >"]
OB["Output Boundary < I >"]

subgraph B2["Boundary"]
  V["View"]
end

UC["Use Case Interactor"]

DAI["Data Access Interface < I >"]

subgraph B3["Boundary"]
  DA["Data Access"]
  DB["Database"]
end

subgraph B4["Boundary"]
  E["Entities"]
end

%% implementation relations
P -. implements .-> OB
UC -. implements .-> IB
DA -. implements .-> DAI

%% dependency relations
C -- depends --> IB
C -- depends --> ID

P -- depends --> VM
P -- depends --> OD

V -- depends --> VM

UC -- depends --> ID
UC -- depends --> OB
UC -- depends --> OD
UC -- depends --> DAI
UC -- depends --> E

DAI -- depends --> E
DA -- depends --> DB

classDef boundary fill:none,stroke:#666,stroke-width:2px,stroke-dasharray: 5 5;
class B1,B2,B3,B4 boundary;
Loading

In fact, the core (library) implementation can follow any other architectural concept, for example, hexagonal, or none at all. It can even be developed in parallel with a reactive client - the client's gateway will connect the two parts later.

Does it have a ViewModel?

Clean Reactive Architecture has a ViewModel, though it is not outlined in the diagram for reasons of simplicity. Here "ViewModel" means what it originally meant - the data structure the presenter returns and the user interface consumes.

clean-reactive-architecture-view-model

mermaid
graph LR

subgraph B1["Boundary"]
  UI["User Interface"]
end

VM["ViewModel < DS >"]
PI["Presenter < I >"]
P["Presenter"]

%% Relationships
UI -- depends --> VM
UI -- depends --> PI
P -- depends --> VM
P -. implements .-> PI

classDef boundary fill:none,stroke:#666,stroke-width:2px,stroke-dasharray: 5 5;
class B1 boundary;
Loading

The ViewModel is just a value: comparable, snapshotable, safe to pass around, trivial to construct for a preview or a test. It has no behavior, no lifecycle, no identity.

It is important to note that every property of a presenter returns its own ViewModel. Let's look at an example:

interface BooksByAuthor {
   authorName: string;
   bookTitle: string;
}

interface UserBooksPresenter {
   userName: string;
   books: BooksByAuthor[];
}

Here userName and books are properties of the UserBooksPresenter interface, each property return own ViewModel - userName a primitive value, books a structured one.

Does it have a repository?

Clean Reactive Architecture has a repository, but the diagram does not have a separate unit for it. The repository is a composite of the gateway and entities units that appears in code - a single implementation that satisfies gateway<I> and holds the entities it serves.

clean-reactive-architecture-repository

mermaid
graph TD

subgraph R1["Repository"]
  E["Entities"]
  G["Gateway"]
end

ER["External Resource"]
UI["User Interface"]
P["Presenter"]
C["Controller"]
PI["Presenter < I >"]
CI["Controller < I >"]
GI["Gateway < I >"]
UC["Use Case Interactor"]

%% implementation relation
P -. implements .-> PI
C -. implements .-> CI
G -. implements .-> GI

%% dependency relation
UI -- depends --> PI
UI -- depends --> CI
C -- depends --> UC
P -- depends --> E
UC -- depends --> E
UC -- depends --> GI
G -- depends --> ER

classDef repository fill:none,stroke:#666,stroke-width:2px,stroke-dasharray: 5 5;
class R1 repository;
Loading

Some implementations let the repository absorb the gateway<I> interface - which is acceptable, except the case where the repository defines the contract rather than consume it. This is not desired, and the development methodology prevents it.

clean-reactive-architecture-repository-with-gateway-interface

mermaid
graph TD

subgraph R1["Repository"]
  E["Entities"]
  G["Gateway"]
  GI["Gateway < I >"]
end

ER["External Resource"]
UI["User Interface"]
P["Presenter"]
C["Controller"]
PI["Presenter < I >"]
CI["Controller < I >"]
UC["Use Case Interactor"]

%% implementation relation
P -. implements .-> PI
C -. implements .-> CI
G -. implements .-> GI

%% dependency relation
UI -- depends --> PI
UI -- depends --> CI
C -- depends --> UC
P -- depends --> E
UC -- depends --> E
UC -- depends --> GI
G -- depends --> ER

classDef repository fill:none,stroke:#666,stroke-width:2px,stroke-dasharray: 5 5;
class R1 repository;
Loading
Where does an alternative driver fit?

The user interface is not a privileged unit, it is a detail. Clean Reactive Architecture treats the user interface as one driver among several.

Driver - a unit that exercises the core: it provides input through a controller, observes the core's state through a presenter, or both.

Normally each driver has its own controller and presenter implementations, which are specific to a particular driver. What is shared is the core: use case, entities and gateway<I> that every driver's controllers and presenters meet.

clean-reactive-architecture-driver-user-interface

mermaid
graph TD

subgraph D1["Driver"]
  UI["User Interface"]
  PI["Presenter < I >"]
  CI["Controller < I >"]
end

G["Gateway"]
ER["External Resource"]
E["Entities"]
P["Presenter"]
C["Controller"]
GI["Gateway < I >"]
UC["Use Case Interactor"]

%% implementation relation
P -. implements .-> PI
C -. implements .-> CI
G -. implements .-> GI

%% dependency relation
UI -- depends --> PI
UI -- depends --> CI
C -- depends --> UC
P -- depends --> E
UC -- depends --> E
UC -- depends --> GI
G -- depends --> ER

classDef driver fill:none,stroke:#666,stroke-width:2px,stroke-dasharray: 5 5;
class D1 driver;
Loading

Common drivers:

  • a test harness drives the core through controllers and observes state to assert against it;
  • a notification or deep link drives the core when an external event arrives;

Let's consider a WebSocket. Incoming messages drive the core: a listener receives them and feeds them in through a controller, exactly as a user gesture would. Outgoing commands take the other path: they leave the core through a gateway, since the socket is also an external resource. The same connection is therefore both a driver (inbound) and an external resource (outbound), sitting on both sides of the core - the core does not know (and does not care) that the input arriving through its controller and the output leaving through its gateway belong to the same socket.

clean-reactive-architecture-driver-web-socket

mermaid
graph TD

subgraph D1["Driver"]
  WSL["WS Listener"]
  CI["Controller < I >"]
end

UI["User Interface"]
PI["Presenter < I >"]
G["Gateway"]
ER["External Resource"]
E["Entities"]
P["Presenter"]
C["Controller"]
GI["Gateway < I >"]
UC["Use Case Interactor"]
WST["WS Transmitter"]

%% implementation relation
P -. implements .-> PI
C -. implements .-> CI
G -. implements .-> GI

%% dependency relation
UI -- depends --> PI
UI -- depends --> CI
C -- depends --> UC
P -- depends --> E
UC -- depends --> E
UC -- depends --> GI
G -- depends --> ER
G -- depends --> WST
WSL -- depends --> CI

classDef driver fill:none,stroke:#666,stroke-width:2px,stroke-dasharray: 5 5;
class D1 driver;
Loading
Where does a Backend for Frontend fit?

A gateway adapts a general-purpose external API into the shape a client actually needs - mapping responses, combining several calls, reshaping data to match the entities the application works with. The more a general-purpose API differs from what the client needs, the more adaptation accumulates in the gateways.

clean-reactive-architecture-without-bff

mermaid
graph TD

subgraph B1["Boundary"]
  GI["Gateway < I >"]
end
G["Gateway (adapt. logic)"]
ER["External Resource"]

subgraph B2["Boundary"]
  GAPI["General Purpose Server-side API"]
end

G -. implements .-> GI
G -- depends --> ER
ER -- depends --> GAPI

%% high-level relations
RCC["Reactive client"]

subgraph B3["Boundary"]
  CGAPI["General Purpose Serve-side API"]
end

RCC -- depends --> CGAPI

classDef boundary fill:none,stroke:#666,stroke-width:2px,stroke-dasharray: 5 5;
class B1,B2,B3 boundary;
Loading

That accumulated adaptation is the signal for a Backend for Frontend (BFF). A BFF is a server-side layer built for one specific client, it performs the mapping and aggregation the client would otherwise do itself. In these terms, the BFF is the difference between the gateway and the general-purpose API - the adaptation moved to the server, where it can be done once and closer to the data.

clean-reactive-architecture-with-bff

mermaid
graph TD

subgraph B1["Boundary"]
  GI["Gateway < I >"]
end
G["Gateway"]
ER["External Resource"]

subgraph B2["Boundary"]
  BFF["BFF (adapt. logic)"]
  GAPI["General Purpose Server-side API"]
end

G -. implements .-> GI
G -- depends --> ER
ER -- depends --> BFF
BFF -- depends --> GAPI

%% high-level relations
subgraph RCC["Reactive client"]
  CGI["Gateway < I >"]
end

subgraph B3["Boundary"]
  CBFF["Reactive Client BFF"]
  CGAPI["General Purpose Serve-side API"]
end

CBFF -. implements .-> CGI
CBFF -- depends --> CGAPI

classDef boundary fill:none,stroke:#666,stroke-width:2px,stroke-dasharray: 5 5;
class B1,B2,B3 boundary;
Loading

The architecture makes this visible. Because adaptation is localized in gateways rather than distributed across the codebase, heavy mappers and aggregations in the gateways are a concrete indication that the work belongs server-side. When a BFF takes over that adaptation, the client's gateways become thin - the BFF returns data already shaped for the client. As the client's needs evolve, the gateways begin accumulating adaptations again, signalling the next round of BFF update.

See also:

S. Newman. Pattern: Backends For Frontends

How does it map to the testing pyramid?

Clear Reactive Architecture maps cleanly onto the testing pyramid.

testing-pyramid

Each level corresponds to a level of unit composition:

  • Unit tests test individual architectural units in isolation - a presenter, a use case, a gateway implementation.
  • Integration tests test architectural units working together - a controller through its use case to a gateway, or a gateway integration with an external resource.
  • End-to-end tests test the full path through the architectural units, from the user interface unit to the external resource unit.

Because the units and their boundaries are explicit, each test level has a clear target: the pyramid's layers are the architecture's layers of composition.