DEV Community

Cover image for When Does an Entity Stop Being a Data Model?
Pavel Nikitin
Pavel Nikitin

Posted on

When Does an Entity Stop Being a Data Model?

Most backend applications start simple.

You have a few tables, a few endpoints, some business logic, and an ORM or repository layer that keeps persistence manageable.

Then the application grows.

A Customer is no longer just:

  • id
  • name
  • email

Now it also needs:

  • an owner
  • authorization rules
  • validation
  • lifecycle states
  • optimistic concurrency
  • events
  • transactional outbox
  • audit information
  • increasingly complex persistence rules

None of these requirements is particularly unusual.

The interesting part is what happens to the architecture.

At some point, the semantics of the Customer entity stop living in one place.

They become distributed across controllers, services, repositories, database constraints, authorization code, background jobs, and event handlers.

The problem is not that the code is distributed.

The problem is that the entity's infrastructure is distributed without an explicit architectural boundary.

Entity gravity

I think of this as entity gravity.

As an application grows, more and more system concerns begin to orbit the same entity.

A typical evolution might look like this:

Each concern is reasonable on its own.

The problem appears when changing one entity-level rule requires coordinating many unrelated parts of the application.

For example, suppose we add optimistic concurrency.

A customer update now needs something like:
PATCH /customers/cus_123

{
 "name": "Acme Corporation",
 "expectedVersion": 7 
}
Enter fullscreen mode Exit fullscreen mode

The application needs to:

  1. authorize the operation
  2. validate the input
  3. check the lifecycle state
  4. verify the expected version
  5. update the record
  6. increment the version
  7. create an event or outbox entry
  8. commit the operation transactionally

The question becomes:

Who owns this coordination?

What if the entity had its own architectural boundary?

Instead of treating the entity as something that is merely passed through application layers, we can introduce an explicit entity infrastructure boundary.

Conceptually:

The important part is not the exact implementation.

The important part is the boundary.

The application still owns its business decisions.

The entity infrastructure owns the common mechanics and guarantees around entities.

A concrete mutation

For example, the application could express an update as:

await customer.update(
 "cus_123",
 {
  name: "Acme Corporation" 
 }, 
 { 
  actor: currentUser, 
  expectedVersion: 7 
 } 
);
Enter fullscreen mode Exit fullscreen mode

Here, actor represents the authenticated user performing the operation.

A possible internal flow is:

If the current version is no longer 7, the operation can fail with a ConcurrencyConflict.

The important distinction with the outbox is that the transaction records the intent to publish the event together with the entity mutation.

Actual event delivery is a separate concern handled by a delivery worker or another delivery strategy after the transaction commits.

This keeps the entity mutation and the record of the event transactionally consistent without pretending that delivery itself is part of the database transaction.

Before and after

Consider a traditional controller where the application coordinates everything itself:

async function updateCustomer(request)
{
  const customer = await repository.find(request.id);
  authorize(request.actor, customer);
  validate(request.data);
  checkLifecycle(customer);
  checkVersion(customer.version, request.expectedVersion);
  await repository.update(customer, request.data);
  await outbox.append(createCustomerUpdatedEvent(customer));
}
Enter fullscreen mode Exit fullscreen mode

There is nothing inherently wrong with this code.

For a small application, this may be exactly the right solution.

The problem appears when the same coordination starts being repeated for many entities and the rules continue to evolve.

With an explicit entity boundary, the application can instead depend on a higher-level contract:

async function updateCustomer(request)
{
  return customerService.update(request.id, request.data, {
    actor: request.actor,
    expectedVersion: request.expectedVersion
  });
}
Enter fullscreen mode Exit fullscreen mode

The difference is not simply fewer lines of code.

The entity-level infrastructure now has an explicit architectural owner.

A practical heuristic

There is no universal threshold for introducing such a boundary.

But one useful signal is this:

If changing one entity-level rule regularly requires edits across five or more unrelated locations, it may be time to investigate a clearer boundary.

Five is not a law.

It is simply a practical heuristic for starting an architectural conversation.

The more important question is whether the same entity-level concerns are repeatedly coordinated across different parts of the system.

What should stay outside the entity boundary?

An explicit boundary should not become a new place to put everything.

Business semantics should remain outside the generic entity infrastructure.

For example:
Customer becomes "qualified" after three successful sales interactions
is a business rule.

The infrastructure can provide lifecycle, persistence, validation, authorization hooks, events, and concurrency mechanisms.

It should not decide what "qualified" means.

If the boundary starts answering domain questions such as:

"is this customer qualified?"

it has crossed its responsibility and risks becoming an Entity God Object.

The boundary should provide infrastructure for the entity, not absorb the entire domain model.

Is this an ORM problem?

Not necessarily.

An ORM can solve persistence and object-relational mapping very well.

But persistence is only one part of the problem.

Entity complexity can also involve:

  • schema
  • validation
  • authorization
  • lifecycle
  • optimistic concurrency
  • transactions
  • events
  • outbox processing

So this is not an argument against ORMs.

It is an argument that the architectural boundary around an entity can be broader than its persistence mapping.

EntityBuilder

EntityBuilder is one concrete implementation of this idea.

Its purpose is to provide reusable infrastructure around entities while keeping application-specific business semantics outside the core.

The implementation provides mechanisms for things such as schema, validation, authorization, lifecycle, persistence, transactions, optimistic concurrency, events, and outbox integration.

The interesting question is therefore not:

"Should every application use EntityBuilder?"

It is:

"Should entity infrastructure have an explicit architectural boundary in the first place?"

EntityBuilder is one answer to that question.

The cost of the boundary

An explicit entity boundary is not free.

It introduces another architectural layer, another integration surface, and potentially additional operational complexity.

For a small CRUD application with a handful of stable entities, introducing such a boundary may simply create more work.

The approach becomes more interesting when:

  • entity-level rules are growing
  • the same infrastructure is being rebuilt repeatedly
  • several applications need similar entity capabilities
  • concurrency and event handling are becoming important
  • entity changes require coordination across many parts of the backend
  • an existing application needs to evolve without a large rewrite

The boundary should reduce architectural friction, not create architecture for architecture's sake.

When not to introduce an entity boundary

You probably do not need one when:

  • the application is straightforward CRUD
  • entity-level rules are few and stable
  • the team is small and owns the whole application
  • there is little need for reusable infrastructure
  • concurrency requirements are minimal
  • events and asynchronous processing are not significant
  • introducing another boundary would add more operational complexity than it removes

A simple application should be allowed to stay simple.


Explore the implementation

EntityBuilder is a concrete implementation of the entity infrastructure approach described in this article.

If you want to see how this boundary can be implemented as reusable infrastructure, you can find EntityBuilder here:

EntityBuilder 1.0 →


How do you test an entity boundary?

Test the boundary contract, not its implementation.

For example:

  • replace the persistence adapter with a fake
  • verify that ConcurrencyConflict is raised when the expected version does not match
  • verify that authorization is checked before the operation proceeds to validation

The goal is to test the guarantees exposed by the boundary rather than couple the tests to its internal implementation.

Frequently asked questions

What is entity architecture?

Entity architecture is an approach where an entity is treated as an explicit architectural boundary rather than only as a data structure or persistence model.

The boundary can contain common infrastructure such as validation, authorization, lifecycle, persistence, concurrency, and event handling.

What is entity infrastructure?

Entity infrastructure is the reusable technical layer responsible for common entity-level mechanics and guarantees.

It should provide infrastructure without absorbing application-specific business semantics.

Is EntityBuilder an ORM?

No.

An ORM primarily addresses object-relational mapping and persistence.

EntityBuilder addresses a broader set of entity infrastructure concerns, including schema, validation, authorization, lifecycle, concurrency, transactions, and events.

How can an existing backend evolve without a rewrite?

One possible approach is to introduce an explicit entity boundary incrementally.

The existing application can continue to own its UI, workflows, and business logic while entity-level infrastructure is gradually moved behind a stable integration boundary.

This does not require replacing the entire backend at once.

What is optimistic concurrency?

Optimistic concurrency assumes that conflicting updates are relatively uncommon.

A mutation includes the version the caller expects to update. If the stored version has changed, the mutation is rejected rather than silently overwriting the newer state.

What is optimistic locking?

Optimistic locking is a common implementation technique for optimistic concurrency, often using a version field that must match before an update succeeds.

What is the transactional outbox pattern?

The transactional outbox pattern stores an event or message record in the same database transaction as the state change that produced it.

A separate delivery process then publishes the stored event.

This helps avoid the situation where the entity update commits successfully but the corresponding event is lost.

How should entity infrastructure be separated from business logic?

Entity infrastructure should provide reusable mechanics and guarantees.

Business logic should remain responsible for domain-specific decisions and workflows.

A useful test is to ask whether a rule describes how the system manages an entity or what the business believes about that entity.

When should you not introduce an entity boundary?

When the additional boundary creates more complexity than it removes.

Simple CRUD applications with stable requirements may not benefit from it. The architectural cost should be justified by growing entity complexity, reuse, concurrency, event processing, or similar requirements.

The real question

At some point, an entity is no longer just data.

It has become a boundary of behavior and guarantees.

The real architectural question is whether that boundary exists explicitly, or whether it is scattered across the rest of the application.

EntityBuilder is one attempt to give that boundary a concrete form.

If your application is reaching the point where changing an entity means changing half the backend, that is the architectural problem worth examining.

Top comments (0)