288 lines
9.7 KiB
Markdown
288 lines
9.7 KiB
Markdown
# Excessive Entities
|
|
|
|
How to keep the `entities` layer clean and avoid over-extracting business
|
|
logic into entities. Excessive entities cause ambiguity (what code belongs
|
|
where), coupling, and constant import dilemmas as code scatters across
|
|
sibling entities.
|
|
|
|
## Why this matters
|
|
|
|
The `entities` layer is one of the lower layers and is widely accessible.
|
|
Every layer except `shared` can import from it. That global nature means
|
|
changes to `entities` propagate widely, requiring careful design to avoid
|
|
costly refactors. Adding an entity is cheap; removing one after many
|
|
consumers depend on it is expensive.
|
|
|
|
## How to keep entities clean
|
|
|
|
### 0. Consider having no entities layer
|
|
|
|
An FSD application without an `entities` layer is still FSD. Skipping the
|
|
layer simplifies the architecture and keeps it available for future scaling.
|
|
|
|
**Thin clients** (where the backend handles most data processing and the
|
|
client mostly exchanges data) usually do not need an entities layer.
|
|
**Thick clients** (significant client-side business logic) are better
|
|
candidates for entities.
|
|
|
|
The classification is not strictly binary. Different parts of the same
|
|
application may behave as thick or thin clients.
|
|
|
|
```text
|
|
// Thin client without entities layer (still valid FSD)
|
|
src/
|
|
app/
|
|
pages/
|
|
dashboard/
|
|
profile/
|
|
shared/
|
|
api/
|
|
ui/
|
|
```
|
|
|
|
### 1. Avoid preemptive slicing
|
|
|
|
FSD v2.1 encourages **deferred decomposition** of slices. Place code in the
|
|
`model` segment of the consuming page, widget, or feature first. Move it to
|
|
`entities` later, when business requirements stabilize and reuse is
|
|
confirmed across multiple consumers.
|
|
|
|
The later code moves to `entities`, the less dangerous the refactor. Code
|
|
in `entities` can affect every higher-layer slice that imports it.
|
|
|
|
```text
|
|
// Iteration 1: code lives where it is used
|
|
pages/profile/
|
|
model/
|
|
profile-validation.ts ← page-specific for now
|
|
|
|
// Iteration 2 (after the same logic is needed in 2+ places):
|
|
entities/profile/
|
|
model/
|
|
profile-validation.ts ← extracted only after reuse is real
|
|
```
|
|
|
|
### 2. Avoid unnecessary entities
|
|
|
|
Do not create an entity for every piece of business logic. Use types from
|
|
`shared/api` and place logic in the `model` segment of the current slice.
|
|
For genuinely reusable business logic, use the `model` segment within an
|
|
entity slice while keeping data definitions in `shared/api`.
|
|
|
|
```text
|
|
shared/
|
|
api/
|
|
endpoints/
|
|
order.ts ← OrderDto type and request functions
|
|
|
|
entities/
|
|
order/
|
|
model/
|
|
apply-discount.ts ← Business logic that uses OrderDto
|
|
index.ts
|
|
```
|
|
|
|
The DTO lives in `shared/api/endpoints/order.ts`. Business logic that
|
|
operates on it (calculating discounts, applying promotions) lives in
|
|
`entities/order/model/`. Do not mirror every API endpoint with a
|
|
corresponding entity.
|
|
|
|
### 3. Exclude CRUD operations from entities
|
|
|
|
CRUD operations involve boilerplate code without significant business
|
|
logic. Putting them in `entities` clutters the layer and obscures the code
|
|
that genuinely matters. Place CRUD in `shared/api`:
|
|
|
|
```text
|
|
shared/
|
|
api/
|
|
client.ts
|
|
endpoints/
|
|
order.ts ← getOrder, createOrder, updateOrder, deleteOrder
|
|
products.ts ← Standard CRUD for products
|
|
cart.ts ← Standard CRUD for cart
|
|
index.ts
|
|
```
|
|
|
|
For complex CRUD with atomic updates, rollbacks, or transactions, evaluate
|
|
whether the operation is genuinely business logic. If so, the `entities`
|
|
layer may be appropriate. If not, keep it in `shared/api`.
|
|
|
|
### 4. Store authentication data in shared
|
|
|
|
Prefer `shared` over creating a `user` entity for auth tokens and session
|
|
DTOs. These are context-specific to authentication and unlikely to be
|
|
reused outside that scope. Wrapping a login response in a `user` entity
|
|
also tends to drag entities into cross-layer imports or `@x` chains,
|
|
complicating the architecture.
|
|
|
|
The Auth guide also documents **In Entities** (a `user` entity) as a
|
|
valid placement when the project already has an entities layer and the
|
|
data is genuinely reused. **In Pages/Widgets** is discouraged for both
|
|
guides.
|
|
|
|
**`shared/auth` (or `shared/api`) is the recommended default.** Choose
|
|
it when:
|
|
|
|
- The project has no entities layer yet
|
|
- Auth state is just a token plus minimal user info (id, email, role)
|
|
- Token management logic (refresh, expiration) is the main concern, not
|
|
user profile data
|
|
|
|
```text
|
|
shared/
|
|
auth/
|
|
use-auth.ts ← Token + minimal user info
|
|
index.ts
|
|
api/
|
|
client.ts ← API client reads token from shared/auth
|
|
endpoints/
|
|
order.ts
|
|
index.ts
|
|
```
|
|
|
|
This approach pairs naturally with an API client middleware that injects
|
|
the token into authenticated requests.
|
|
|
|
**A `user` entity is the right call when:**
|
|
|
|
- The project already has an entities layer
|
|
- Auth and profile data are tightly coupled (current user info is reused
|
|
across pages for non-auth purposes like comments, posts, mentions)
|
|
- Token management has complex business logic (invalidation policies,
|
|
multi-device session tracking) that benefits from co-location with the
|
|
user model
|
|
|
|
```text
|
|
entities/
|
|
user/
|
|
model/
|
|
current-user.ts ← Token + full user model + business logic
|
|
user.ts ← Generic user type, used for other users too
|
|
api/
|
|
get-current-user.ts
|
|
index.ts
|
|
```
|
|
|
|
When using the entity approach, the API client (in `shared/api`) needs
|
|
access to the token without violating the import rule. The official Auth
|
|
guide describes three solutions: pass the token manually on each request,
|
|
expose it through a context or `localStorage` with the key kept in
|
|
`shared/api`, or inject the token into the API client whenever the entity
|
|
store updates.
|
|
|
|
**Pages and widgets are discouraged.** Avoid placing the token store in a
|
|
page's `model/` segment or in a widget. App-wide state belongs in Shared
|
|
or Entities, not in route-bound or block-bound layers.
|
|
|
|
### Decision summary
|
|
|
|
| Project state | Recommended location |
|
|
| --- | --- |
|
|
| No entities layer (yet), simple token + minimal user info | `shared/auth` |
|
|
| Entities layer exists, auth and profile tightly coupled | `entities/user` |
|
|
| Complex token logic, no profile reuse yet | `shared/auth` (split from `shared/api`) |
|
|
| Token storage in a single page or widget | Avoid; promote to Shared or Entities |
|
|
|
|
A `user` entity created **only** to wrap a login response is premature.
|
|
Wait until profile data is consumed for non-auth purposes (avatars in
|
|
comments, names in posts) before introducing the entity.
|
|
|
|
### 5. Minimize cross-imports
|
|
|
|
FSD permits cross-imports between entities via `@x`, but they introduce
|
|
technical issues including circular dependencies. Design entities within
|
|
**isolated business contexts** so cross-imports become unnecessary.
|
|
|
|
**Non-isolated context (avoid):**
|
|
|
|
```text
|
|
entities/
|
|
order/
|
|
@x/
|
|
model/
|
|
order-item/
|
|
@x/
|
|
model/
|
|
order-customer-info/
|
|
@x/
|
|
model/
|
|
```
|
|
|
|
Three sibling entities all referencing each other through `@x`. This is a
|
|
sign that the boundaries are wrong.
|
|
|
|
**Isolated context (preferred):**
|
|
|
|
```text
|
|
entities/
|
|
order-info/
|
|
model/
|
|
order-info.ts ← order, items, and customer info together
|
|
index.ts
|
|
```
|
|
|
|
One entity encapsulates the related logic. No `@x`, no cross-imports,
|
|
no circular dependency risk.
|
|
|
|
The general rule: when several entities have `@x` dependencies on each
|
|
other, treat that as a signal to merge the boundaries, not as something to
|
|
manage.
|
|
|
|
## Decision tree for AI agents
|
|
|
|
```text
|
|
A new piece of business logic needs a home.
|
|
│
|
|
├─ Is the project a thin client?
|
|
│ └─ YES → Skip entities. Place in shared/ + page model.
|
|
│
|
|
├─ Is the logic used in only one place right now?
|
|
│ └─ YES → Keep in the consuming slice's model/. Defer extraction.
|
|
│
|
|
├─ Is it a CRUD operation without business meaning?
|
|
│ └─ YES → shared/api/endpoints/<resource>.ts
|
|
│
|
|
├─ Is it auth data (tokens, session, login DTOs)?
|
|
│ ├─ Project has no entities layer yet?
|
|
│ │ └─ YES → shared/auth/
|
|
│ ├─ Auth and profile data tightly coupled, entities layer exists?
|
|
│ │ └─ YES → entities/user/
|
|
│ └─ Otherwise → shared/auth/ (default).
|
|
│ Avoid placing in a page or widget.
|
|
│
|
|
├─ Is it just a TypeScript type for an API response?
|
|
│ └─ YES → shared/api/. No entity needed for types alone.
|
|
│
|
|
└─ Is it reusable domain logic confirmed in 2+ consumers?
|
|
└─ YES → Create entities/<name>/model/.
|
|
Verify the boundary is isolated and does not require @x
|
|
to communicate with sibling entities.
|
|
```
|
|
|
|
## Anti-patterns
|
|
|
|
- **Creating entities preemptively.** Wait for confirmed reuse in 2+
|
|
consumers, not anticipated reuse.
|
|
- **Mirroring every API endpoint with an entity.** API endpoints belong in
|
|
`shared/api`. Entities exist for business logic, not for paralleling the
|
|
backend structure.
|
|
- **Creating a `user` entity *only* to wrap a login response.** A `user`
|
|
entity is justified when profile data is reused across non-auth flows
|
|
(avatars in comments, names in posts) or when token logic is genuinely
|
|
tied to user business logic. Until that reuse appears, `shared/auth`
|
|
is simpler. Storing tokens in a page or widget is discouraged regardless
|
|
of the project shape.
|
|
- **Splitting one domain into many entities (`order`, `order-item`,
|
|
`order-customer-info`).** This produces `@x` chains. Merge into a single
|
|
isolated context (`order-info` or `order`).
|
|
- **Putting CRUD wrappers in entities.** They clutter the layer. CRUD goes
|
|
in `shared/api/endpoints/`.
|
|
|
|
## See also
|
|
|
|
- `references/cross-import-patterns.md`: how to handle cross-imports when
|
|
they appear, and why `@x` is a last resort.
|
|
- `references/layer-structure.md`: layer responsibilities and the entities
|
|
segment shape.
|