Files
main_frontend/.agents/skills/feature-sliced-design/references/excessive-entities.md
T
Daniil 21e936a827
dev / deploy (push) Successful in 2m15s
compute / deploy (push) Has been cancelled
chore: agentic upgrade
2026-05-17 02:11:33 +03:00

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.