DEV Community

Saras Growth Space
Saras Growth Space

Posted on

LLD Domain Modeling: Designing a Ride Sharing System (Applying Domain Modeling in a Real System)

So far in this series, we’ve learned:

  • Entities
  • Value Objects
  • Aggregates
  • Invariants
  • State transitions
  • Service vs Entity separation
  • Bounded Contexts

Now it’s time to apply everything together in a real Low-Level Design problem.

Let’s design a simplified Ride Sharing system like Uber/Ola.

Not from a “class diagram” perspective.

But from a domain modeling perspective.


Step 1 — Start With Business Flows, Not Classes

A common beginner mistake is:

Ride.java
Driver.java
User.java
Payment.java
Enter fullscreen mode Exit fullscreen mode

before understanding system behavior.

Strong LLD starts differently.

First identify:

  • workflows
  • lifecycle
  • business rules
  • consistency needs

Core Ride Flow

Our simplified flow:

User requests ride
→ Driver gets assigned
→ Ride starts
→ Ride completes
→ Payment happens
Enter fullscreen mode Exit fullscreen mode

Now we can begin modeling.


Step 2 — Identify Entities

Ask:

“Which business objects have identity and lifecycle?”


Ride

Ride has:

  • rideId
  • lifecycle
  • status transitions
  • business tracking

So Ride is clearly an Entity.


Driver

Driver has:

  • driverId
  • availability state
  • active ride tracking

Also an Entity.


User

User identity matters across rides and payments.

Also an Entity.


Step 3 — Identify Value Objects

Now ask:

“Which objects are defined only by value?”


Location

Location(28.61, 77.20)
Enter fullscreen mode Exit fullscreen mode

We care about:

  • coordinates not
  • identity

So Location becomes a Value Object.


Fare

Fare(250)
Enter fullscreen mode Exit fullscreen mode

Again:

  • no independent lifecycle
  • no business identity

Fare is also a Value Object.


Step 4 — Identify Invariants

Now we discover business rules.

Examples:

  • Ride cannot complete before starting
  • Driver cannot accept multiple active rides
  • Fare cannot become negative
  • Payment should happen only once

These are invariants.

The system must always protect them.


Step 5 — Identify Aggregate Boundaries

Now ask:

“Which objects must stay consistent together?”

This is where many beginner designs become weak.


Ride Aggregate

Ride Aggregate
 ├── Ride (Root)
 ├── Fare
 ├── PickupLocation
 ├── DropLocation
 └── DriverReference
Enter fullscreen mode Exit fullscreen mode

Ride becomes the Aggregate Root because:

  • it controls lifecycle
  • it validates transitions
  • it protects consistency

External systems should modify ride state only through Ride.


Step 6 — Protect State Transitions

Weak systems expose state directly:

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

Dangerous.

Now invalid transitions become easy.

Instead:

class Ride {

    void completeRide() {

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

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

Now the Entity protects lifecycle correctness itself.


Step 7 — Separate Entity Logic From Workflow Logic

Another common beginner mistake:

putting all logic inside services.

Example:

class RideService {

    void completeRide(Ride ride) {

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

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

Over time:

  • services become massive
  • logic gets duplicated
  • rules scatter everywhere

Better Separation

Ride Entity

Responsible for:

  • protecting state
  • validating transitions
  • enforcing invariants

RideService

Responsible for:

  • orchestration
  • payment coordination
  • notifications
  • driver updates

Example:

class RideService {

    void completeRide(Ride ride) {

        ride.completeRide();

        paymentService.process(ride);
        notificationService.notify(ride);
        driverService.markAvailable(ride.getDriver());
    }
}
Enter fullscreen mode Exit fullscreen mode

This creates cleaner boundaries.


Step 8 — Think About Future Scaling

As systems grow:

  • pricing becomes complex
  • matching evolves separately
  • payments become independent
  • driver systems scale differently

This is where bounded thinking becomes useful.

Example contexts:

Ride Matching Context
Payments Context
Driver Management Context
Enter fullscreen mode Exit fullscreen mode

Each context evolves independently.


Final Domain Model

Entities
- Ride
- Driver
- User

Value Objects
- Location
- Fare

Aggregate
- Ride Aggregate

Invariants
- valid ride lifecycle
- driver consistency

Services
- RideService
- PaymentService
- NotificationService
Enter fullscreen mode Exit fullscreen mode

Now the design is not just “object-oriented”.

It is:

  • behavior-oriented
  • consistency-aware
  • lifecycle-aware
  • business-driven

The Most Important Insight

Most beginners think system design means:

“creating classes and APIs.”

But real LLD is actually about:

  • modeling business behavior
  • protecting consistency
  • controlling state evolution
  • assigning responsibilities correctly

That is why strong domain models feel stable even as systems grow.

Because good design is not created by connecting objects randomly.

It is created by carefully modeling how the business actually behaves.

Top comments (0)