DEV Community

Saras Growth Space
Saras Growth Space

Posted on

LLD Domain Modeling: Services vs Entities (Separating Workflow from Business Logic)

Once you start modeling:

  • business rules
  • invariants
  • state transitions

another important design question appears:

“Where should this logic actually live?”

Inside Entities?

Inside Services?

Inside Controllers?

This is one of the most important design decisions in Low-Level Design because poor separation eventually creates:

  • bloated services
  • weak domain models
  • duplicated logic
  • inconsistent behavior
  • difficult maintenance

And many systems slowly become unmanageable because this boundary is unclear.


The Core Problem

Consider a Ride Sharing system.

When completing a ride:

  • ride state changes
  • payment is processed
  • notifications are sent
  • driver status changes

Now ask carefully:

Which part belongs to the Ride itself?

And which part belongs to workflow orchestration?

That distinction is the heart of Entity vs Service separation.


Entities Should Protect Business State

Entities are responsible for:

  • maintaining valid state
  • enforcing business rules
  • protecting invariants
  • controlling state transitions

Example:

class Ride {

    void completeRide() {

        if(status != IN_PROGRESS) {
            throw new InvalidRideStateException();
        }

        status = COMPLETED;
    }
}
Enter fullscreen mode Exit fullscreen mode

The Ride Entity ensures:

  • invalid transitions are rejected
  • lifecycle remains consistent

This logic belongs inside the domain model itself.


Services Should Coordinate Workflows

Services are responsible for:

  • orchestration
  • coordination
  • external communication
  • multi-entity interactions

Example:

class RideService {

    void completeRide(Ride ride) {

        ride.completeRide();

        paymentService.processPayment(ride);
        notificationService.sendRideCompleted(ride);
        driverService.markAvailable(ride.getDriver());
    }
}
Enter fullscreen mode Exit fullscreen mode

The Service coordinates workflow.

The Entity protects business consistency.

That separation creates clarity.


The “Fat Service” Problem

A very common beginner design looks like this:

class RideService {

    void completeRide(Ride ride) {

        if(ride.status != IN_PROGRESS) {
            throw new InvalidRideStateException();
        }

        ride.status = COMPLETED;
    }
}
Enter fullscreen mode Exit fullscreen mode

Initially this seems harmless.

But over time:

  • all business rules move into services
  • entities become passive data holders
  • logic gets duplicated across workflows

Eventually services become extremely large and difficult to maintain.

This is often called the Anemic Domain Model problem.


Entities Should Not Become “God Objects” Either

The opposite mistake is equally dangerous.

Example:

class Ride {

    void completeRide() {

        paymentService.process();
        notificationService.send();
        analyticsService.track();

        status = COMPLETED;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now the Entity:

  • depends on infrastructure
  • handles orchestration
  • mixes domain logic with external concerns

This creates tight coupling and poor separation.

Entities should not control the entire application workflow.


The Real Separation

A useful mental model is:

Responsibility Best Place
Business rules Entity
State protection Entity
Lifecycle validation Entity
Workflow coordination Service
External integrations Service
Cross-entity orchestration Service

This boundary keeps systems balanced.


Example — Amazon Cart

Cart Entity

Responsible for:

  • validating quantity
  • calculating totals
  • applying business rules

Example:

cart.addItem(product, quantity);
cart.calculateTotal();
Enter fullscreen mode Exit fullscreen mode

Checkout Service

Responsible for:

  • processing payment
  • creating order
  • sending confirmation
  • coordinating inventory updates

Example:

checkoutService.checkout(cart);
Enter fullscreen mode Exit fullscreen mode

This separation makes the design cleaner and easier to evolve.


Strong Systems Separate “Behavior” From “Workflow”

This is an important distinction.

Entities manage:

how business state changes safely.

Services manage:

how system operations flow across multiple components.

Both are necessary.

But mixing them creates confusion.


Weak LLD Thinking

“Put all logic inside services.”

or

“Put everything inside entities.”

Both are extremes.


Strong LLD Thinking

“Which logic belongs to the business object itself… and which logic belongs to application coordination?”

That question leads to far better designs.


Why This Matters So Much at Scale

As systems grow:

  • workflows become more complex
  • integrations increase
  • multiple services interact
  • business rules evolve

Without clear separation:

  • changes become risky
  • duplication spreads
  • debugging becomes painful

Strong responsibility boundaries slow down system chaos.


Real Domain Models Protect Themselves

A good Entity should not allow invalid behavior.

A good Service should not own business correctness.

That balance is one of the biggest indicators of strong Low-Level Design maturity.

Because good systems are not built by randomly distributing logic.

They are built by carefully assigning responsibilities to the right layers.

And once responsibility boundaries become clear, the next important challenge becomes:

modeling how business objects safely evolve from one state to another over time.

Top comments (0)