skip to content

What is the 'entity service' (a.k.a. CRUD-service) anti-pattern when decomposing a system into microservices, and why does it undermine the benefits microservices are supposed to deliver?

level: middleimportance: must knowfreq 65%

answer

  1. CRUD over table, not capability
  2. logic lives in callers, not service
  3. ER diagram != bounded context
  4. orchestrator relocates coupling
  5. verbs not nouns in API

basics

~20 s

An entity service is a microservice built around one database table (like 'Customer' or 'Order') that just exposes create/read/update/delete endpoints for that table, with no real business logic. It looks like a clean split by 'noun,' but it pushes all the actual decision-making into whichever caller uses it, so business logic ends up duplicated or scattered across many services instead of owned by one.

solid answer

~40 s

Entity services decompose around data nouns (Customer, Order, Product) instead of business capabilities (Checkout, Fulfillment, Pricing), exposing thin CRUD APIs over a table. The problem is that business logic - validation rules, workflows, invariants - doesn't live anywhere near the data, so every caller that needs to do something meaningful ends up reimplementing rules against the raw CRUD API, which duplicates logic and lets invariants drift out of sync across services. It also reintroduces shared-database-style coupling, because callers must know the entity's internal shape and issue multiple calls to accomplish one business operation, effectively turning the 'service boundary' into a thin proxy over a table rather than a real bounded context.

go deeper

for a junior

Should recognize that a service exposing only create/read/update/delete on a table isn't necessarily doing anything meaningful with business rules, and that this can cause problems.

for a middle

Should explain concretely where the business logic ends up (callers/orchestrators) when a service is 'entity-only,' and why that causes duplication.

for a senior

Should connect the smell to bounded-context / DDD thinking and propose renaming the API surface around capabilities, with invariant enforcement moved server-side.

for a principal

Should discuss how this smell tends to reappear at scale as new teams onboard and default to CRUD, and propose an API-design review practice or template that catches it before it ships.

## What an entity service is An entity service (also called a CRUD service or 'database as a service') is a microservice whose API surface mirrors a database table's columns almost 1:1 — endpoints like `POST /customers`, `GET /customers/{id}`, `PUT /customers/{id}`, `DELETE /customers/{id}` — with little or no business logic behind them beyond basic field validation. It arises when a team decomposes a monolith by looking at its entity-relationship diagram and drawing service boundaries around nouns rather than around the verbs and workflows that operate on those nouns: | Boundaries drawn around | Examples | |---|---| | nouns | Customer, Order, Product, Invoice | | the verbs and workflows that operate on those nouns | PlaceOrder, ApplyDiscount, ShipItem | The result looks superficially like good decomposition — each service 'owns' one entity's data — but it fails the test of what a bounded context actually needs to encapsulate: not just data, but the rules and workflows that keep that data valid and meaningful. ## Why teams reach for it This pattern is seductive because it maps directly onto how relational databases are already organized, so it's the path of least resistance when migrating a monolith: 1. take each table (or table cluster) 2. wrap it in a service 3. call the decomposition done It also mirrors how many teams think about REST APIs, which are conventionally taught around CRUD resources. But an ER diagram encodes data structure, not business capability boundaries — a 'Customer' table is touched by registration, billing, support, and marketing workflows that have almost nothing to do with each other and arguably belong in different bounded contexts, each with its own view of what a 'customer' even means. ## Where the business logic ends up The core cost is that business logic has to live somewhere, and if the entity service doesn't own it, every consumer does. A checkout workflow that needs to validate a customer's credit limit before creating an order now has to: 1. fetch the customer entity 2. apply the credit-limit rule itself 3. then call the order entity service to create the record If a second consumer (say, a support tool that lets an agent manually create orders) needs the same rule, it reimplements it independently. Over time these duplicated rules drift: one caller updates its validation logic for a new promo, the other doesn't, and now the system enforces inconsistent invariants depending on which caller you went through. This is the same coupling problem microservices are meant to solve, just moved from 'shared database' to 'shared business rule scattered across service consumers.' ## Recognizable symptoms In practice, entity-service architectures tend to produce a few recognizable symptoms. 1. **First, 'god' orchestrator services or BFFs emerge** that call several entity services in sequence and stitch the real business logic together in one place — which just relocates the monolith's coupling into an orchestration layer instead of eliminating it, and that orchestrator becomes a single point of failure and a bottleneck for changes. 2. **Second, invariant violations creep in**: an order can end up referencing a customer that was deleted, or a total that doesn't match its line items, because no service is actually responsible for enforcing those rules atomically — the entity services just persist whatever they're told. 3. **Third, chatty, multi-call workflows appear** because any nontrivial operation touches several entity services, and there's no transactional boundary around the whole operation, so partial failures leave data inconsistent unless every caller carefully implements compensation logic. ## The fix, and the smell test A commonly cited fix, drawn from Domain-Driven Design and popularized in Sam Newman's and Eric Evans's work, is to decompose around bounded contexts and aggregates instead of raw entities. Instead of a single 'Order' entity service exposing generic CRUD, you'd have an 'Order Management' service that exposes business operations — `PlaceOrder`, `CancelOrder`, `ApplyDiscount` — and internally enforces the invariants (stock availability, valid discount codes, total consistency) as part of those operations, with the underlying table as an implementation detail nobody outside the service touches directly. A useful practical smell test: if a service's endpoints read like a database schema (nouns and CRUD verbs) rather than like a domain expert's vocabulary (`PlaceOrder`, `ShipPackage`, `IssueRefund`), it's probably an entity service in disguise, and the fix is to pull the business logic that currently lives in callers back inside the service and re-expose it as capability-oriented endpoints.

  • How would you refactor an entity service like a generic 'Order' CRUD API into something capability-oriented?
    Identify the actual business operations performed against orders - PlaceOrder, CancelOrder, ApplyDiscount, ShipOrder - and expose those as the API instead of raw CRUD. Move the validation and invariant-enforcement logic that currently lives in callers into the service itself, so the service becomes the single place those rules are enforced. The underlying table becomes a private implementation detail that only this service touches directly.
  • Is exposing any CRUD endpoint always a sign of the entity-service anti-pattern?
    No - some services legitimately manage simple reference or lookup data (e.g., a 'country codes' service) where CRUD really is the whole capability and there's no meaningful business workflow beyond storing and retrieving records. The smell is specifically when a service represents a rich business concept (Order, Customer, Account) but its API stops at CRUD while real business rules about that concept live elsewhere.
  • What's the relationship between the entity-service anti-pattern and the 'shared database' anti-pattern?
    They're closely related: a shared database lets multiple services read/write the same tables directly, bypassing any service boundary entirely, while entity services keep a boundary but make it so thin (pure CRUD) that callers still have to reconstruct business logic externally. Both end up with business rules and data consistency enforced outside the component that owns the data, just via different mechanisms.

It's like a car dealership that only sells raw parts (engine, wheels, seats) and expects every customer to assemble the car themselves in the parking lot - technically each part is 'owned' by a supplier, but nobody actually owns making sure the finished car works, so every buyer ends up reinventing assembly and some get it wrong.

saying these in an interview costs you the question

  • Describes decomposition purely by drawing boxes around ER-diagram tables
  • Can't distinguish a legitimate simple lookup CRUD service from an entity-service smell
  • Doesn't mention where business logic ends up living once it's pulled out of the entity service
  • Proposes fixing it by adding another layer of CRUD services rather than capability-oriented ones
  • No mention of invariant/consistency drift as a consequence

context