This article shares the concept and implementation of the entities unit in frontend applications within Clean Architecture.
Repository with example:
https://github.com/harunou/react-tanstack-react-query-clean-architecture
Table of Contents
The entities unit is an aggregate that maintains a collection of enterprise business entities and/or application business entities and their states. The state of the entities unit is the value of that collection at a given point in time, typically represented as an object structure. Other units — use case interactors, transactions, selectors, and presenters — read from or write to the entities unit, but the entities unit itself holds no logic: it is purely a state container.
The entities unit needs a store utility or library to hold its state at runtime — useState, useReducer, Zustand, Redux, MobX, and so on. The store utility is infrastructure: a mechanism for keeping state in memory and reacting to changes, not an architectural concept in itself.
In practice, the entities unit evolves through two implementation styles as the application grows:
----------------- --------------------
| inline entities | ---> | extracted entities |
----------------- --------------------
Both styles can further split enterprise and application business entities into separate collections when the application warrants it.
Entity Modeling
Modeling starts from identifying the minimal data needed for the application to work correctly. Good starting points include the public interfaces of user interface units, business logic requirements, and possibly API contracts. The important rule is that entities should serve the frontend application's purpose — and nothing more.
There are two categories of entities maintained by the entities unit:
-
Enterprise business entities — objects encapsulating enterprise business rules and data that would exist even without this specific application (e.g.
OrderEntity,ItemEntity). These rules and data are high-level, rarely change, and are independent of any particular application. -
Application business entities — objects encapsulating application-specific rules and data: how business concepts are presented to users, interaction flows, UI state, and application-specific behaviors (e.g.
isLoading,selectedOrderId). These are more likely to change than enterprise rules.
Which store utility manages the entities unit is a secondary concern. The architectural boundaries stay the same regardless of whether the state is held by useState or Zustand.
Inline Entities
In a simple component, the entities unit can be implemented inline using React's own state hooks. The entity shapes are defined as interfaces; useState is the store utility that keeps their state alive during the component's lifetime.
Other units inline in the same component — the use case interactor, presenter, and controller — interact with the entities unit through the state hooks directly.
interface ItemEntity {
id: string;
productId: string;
quantity: number;
}
interface OrderEntity {
id: string;
userId: string;
items: ItemEntity[];
}
export const TotalOrdersAndItemsAmount: FC = () => {
// entities unit: enterprise business entities — managed by useState
const [orders, setOrders] = useState<OrderEntity[]>([]);
// entities unit: application business entity — managed by useState
const [isLoading, setIsLoading] = useState(false);
const ordersGateway = useOrdersGateway();
// inline use case interactor — orchestrates entities, gateway, and state transitions
const loadOrders = async () => {
// transitions isLoading to valid state: true
setIsLoading(true);
const orders = await ordersGateway.getOrders();
// transitions orders to valid state: loaded data
setOrders(orders);
// transitions isLoading to valid state: false
setIsLoading(false);
};
// inline presenter — derives output data from entity state via selectors
const totalOrdersAmount = orders.length;
const totalItemsAmount = orders.reduce((acc, order) => acc + order.items.length, 0);
// inline controller — converts user input into use case invocations
const refreshButtonClicked = () => {
loadOrders();
};
const componentMounted = () => {
loadOrders();
};
// lifecycle hook
useEffect(() => {
componentMounted();
}, []);
// user interface
return (
<>
<button onClick={refreshButtonClicked}>Refresh</button>
{isLoading && <div>Loading....</div>}
<div>Total orders Amount: {totalOrdersAmount}</div>
<div>Total items Amount: {totalItemsAmount}</div>
</>
);
};
OrderEntity and ItemEntity are the enterprise business entities. isLoading is an application business entity. Together they form the entities unit's state. useState is just the utility holding that state.
Extracted Entities
When the entities unit's state needs to be shared across multiple components, it must be extracted into a store utility that lives outside any single component. Zustand, Redux, MobX, and similar libraries fill this role. The entity shapes and the architectural role of the entities unit do not change — only the store utility managing the state changes.
// File: entities/order.ts
// Enterprise business entity shapes — independent of any store utility
export interface ItemEntity {
id: string;
productId: string;
quantity: number;
}
export interface OrderEntity {
id: string;
userId: string;
items: ItemEntity[];
}
// File: store/ordersStore.ts
// Zustand as the store utility for the enterprise business entities collection
import { create } from "zustand";
import { OrderEntity } from "../entities/order";
type OrdersStore = {
orders: OrderEntity[];
setOrders: (orders: OrderEntity[]) => void;
};
export const useOrdersStore = create<OrdersStore>((set) => ({
orders: [],
setOrders: (orders) => set({ orders }),
}));
// File: TotalOrdersAndItemsAmount.tsx
export const TotalOrdersAndItemsAmount: FC = () => {
// entities unit: enterprise business entities — now managed by Zustand
const orders = useOrdersStore((state) => state.orders);
const setOrders = useOrdersStore((state) => state.setOrders);
// entities unit: application business entity — still managed by useState (local scope)
const [isLoading, setIsLoading] = useState(false);
const ordersGateway = useOrdersGateway();
// inline use case interactor — orchestrates entities, gateway, and state transitions
const loadOrders = async () => {
setIsLoading(true);
const orders = await ordersGateway.getOrders();
setOrders(orders);
setIsLoading(false);
};
// inline presenter — derives output data from entity state via selectors
const totalOrdersAmount = orders.length;
const totalItemsAmount = orders.reduce((acc, order) => acc + order.items.length, 0);
// inline controller — converts user input into use case invocations
const refreshButtonClicked = () => {
loadOrders();
};
const componentMounted = () => {
loadOrders();
};
// lifecycle hook
useEffect(() => {
componentMounted();
}, []);
// user interface
return (
<>
<button onClick={refreshButtonClicked}>Refresh</button>
{isLoading && <div>Loading....</div>}
<div>Total orders Amount: {totalOrdersAmount}</div>
<div>Total items Amount: {totalItemsAmount}</div>
</>
);
};
OrderEntity and ItemEntity are defined in their own file, entirely independent of Zustand. Swapping Zustand for any other store utility would leave the entity definitions untouched — only the store adapter changes. The application business entity isLoading remains inline with useState because its scope is local to this component; there is no need to extract it until it must be shared.
Q&A
How to test entities?
The entities unit can be tested both in integration with other units and in isolation. When testing units that interact with the entities unit — use case interactors, transactions, selectors, or presenters — the store utility can be mocked or replaced with a simple in-memory object. Integration tests can exercise the full entities unit through the real store utility, following the guidelines provided by the library.
Should enterprise and application business entities be kept separate?
As practice shows, it is worth separating them. In the beginning, both can live together in the same state — a single store slice. As the entities unit grows, separating enterprise business entities (e.g. orders) from application business entities (e.g. isLoading, selectedOrderId) makes the state much easier to observe and reason about. Enterprise business entities represent data that exists independently of this application; application business entities represent data that exists only to serve it. Keeping them separate makes each category's purpose clear.
Conclusion
The entities unit is an aggregate that maintains collections of enterprise and application business entities and their states. Other units — use case interactors to orchestrate flows, transactions to transition state between valid states, selectors and presenters to derive output — all depend on the entities unit, but the entities unit itself holds no logic. A store utility or library is the infrastructure that keeps its state in memory: a detail of implementation, not of architecture. Starting with an inline entities unit is sufficient for simple cases; extracting it into a shared store utility is the natural next step as the application grows. In either case, the entity definitions stay at the center of the architecture, stable and independent of the mechanism that stores them.
Top comments (0)