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

9.7 KiB

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.

// 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.

// 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.

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:

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
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
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):

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):

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

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.