Hello. In the previous lesson, you turned an ambiguous LLD prompt into a compact requirements sheet: capabilities, business rules, constraints, scope boundaries, and likely change points. That sheet is now your evidence for deciding which concepts deserve objects and what kind of objects they should be.
This lesson focuses on a core modeling decision: classify a domain concept as an entity, a value object, or a service. The goal is not to label every noun in the prompt. It is to give state, behavior, and identity a sensible home—an interview skill that makes later class diagrams and invariants much easier to defend.
The central question: what makes this concept “the same”?
A domain model is not a database schema with class syntax. A prompt may mention customers, tickets, locations, fees, rules, reservations, notifications, and time. Some of those are objects, some are values, and some are actions. The classification depends on how the business distinguishes one instance from another.
Use this first principle:
A concept is an entity when the business must recognize it as the same thing over time.
A concept is a value object when only its current attribute values matter.
A domain service represents a meaningful business operation that does not naturally belong to one entity or value object.
Consider two parking tickets:
- They may have the same entry time, vehicle type, and price so far.
- They are still different tickets because each represents a distinct parking session and must be found, updated, and closed independently.
So, ParkingTicket is an entity.
Now consider two values representing $10 USD:
- If both have amount
10and currencyUSD, they are interchangeable for most pricing calculations. - Neither needs its own history, lookup endpoint, or lifecycle.
So, Money is a value object.
The distinction is about domain semantics, not object-oriented syntax. Both entities and value objects can be implemented as Python classes, contain validation, and expose methods.
Use Tactical DDD to Design Microservices - Azure Architecture Center
Read the Microsoft Azure Architecture Center’s explanation of entities and value objects. It establishes the identity, equality, mutability, and behavior rules that make these classifications useful in real backend systems.
In the “Entities” section, read the entity criteria. Focus on the distinction between an entity’s stable identity and its changing attributes, and on why entities should own relevant business behavior. Then continue into “Value objects” and read value object behavior. Notice that immutability is the normal default and that the same real-world concept can be classified differently when business requirements change.
Entities: identity and lifecycle
An entity has an identity that persists while its attributes and state may change. The identity might be a ticket number, order ID, account ID, or reservation ID. It can be a natural identifier meaningful to the business, such as ORD-1042, or a generated identifier such as a UUID.
The key test is not merely “Does it have an ID column?” Instead, ask:
- Will the system retrieve or refer to this specific instance later?
- Can its attributes change while it remains the same domain object?
- Would two instances with identical attributes still represent distinct things?
- Does it have a lifecycle or state transitions to protect?
For the scoped parking-lot specification from the prior lesson, these are usually entities:
| Concept | Why it is an entity |
|---|---|
ParkingLot | A specific lot has its own identity, capacity, and operational state. |
ParkingSpot | Spot F2-18 remains that spot whether occupied or available. |
ParkingTicket | It tracks one specific parking session from issuance through closure. |
Vehicle | If the system tracks a particular visiting or registered vehicle by plate, it has identity in this domain. |
Identity controls equality. Two ticket objects that both represent ticket T-100 should be treated as the same domain ticket, even if one copy was loaded before the ticket was closed and the other was loaded afterward. Conversely, tickets T-100 and T-101 are distinct even if all their visible attributes happen to match.
An entity also should not be a passive bag of fields. If a ticket must not be closed twice, the ticket should expose a behavior such as close(exit_time) that validates and performs that transition. Putting that rule only in a broad TicketService makes it easier for another caller to bypass the rule and mutate fields directly. This “data objects plus giant service classes” style is often called an anemic domain model.
A small illustrative entity might look like this:
from datetime import datetime
class TicketAlreadyClosedError(Exception):
pass
class ParkingTicket:
def __init__(
self,
ticket_id: str,
vehicle_plate: str,
spot_id: str,
entry_time: datetime,
) -> None:
self.ticket_id = ticket_id
self.vehicle_plate = vehicle_plate
self.spot_id = spot_id
self.entry_time = entry_time
self.exit_time: datetime | None = None
def close(self, exit_time: datetime) -> None:
if self.exit_time is not None:
raise TicketAlreadyClosedError(self.ticket_id)
if exit_time < self.entry_time:
raise ValueError("Exit time cannot precede entry time.")
self.exit_time = exit_time
def __eq__(self, other: object) -> bool:
return (
isinstance(other, ParkingTicket)
and self.ticket_id == other.ticket_id
)
This is not yet a complete parking-lot design. It illustrates the classification: ParkingTicket has a stable identity, changes over time, and protects a rule that belongs to the ticket itself.
Value objects: values that can be replaced
A value object is defined entirely by its attributes. It has no independent identity or lifecycle. When its data changes, you conceptually have a new value.
Common backend examples include:
Money(amount, currency)Address(street, city, postal_code)DateRange(start, end)GeoLocation(latitude, longitude)Dimensions(length, width, height)LicensePlate(value)when used only as an immutable identifier valueSpotSizeorVehicleTypewhen an enum is sufficient
Two equal value objects are interchangeable. That gives them value equality:
The usual implementation choice in Python is a frozen dataclass. Immutability prevents a caller from silently changing a value that another object is also using.
from dataclasses import dataclass
from decimal import Decimal
@dataclass(frozen=True)
class Money:
amount: Decimal
currency: str
def __post_init__(self) -> None:
if self.amount < Decimal("0"):
raise ValueError("Money amount cannot be negative.")
def add(self, other: "Money") -> "Money":
if self.currency != other.currency:
raise ValueError("Cannot add different currencies.")
return Money(
amount=self.amount + other.amount,
currency=self.currency,
)
Calling add produces a new Money rather than modifying either input. The value object still has behavior; immutability does not mean “only store fields.”

The highlighted Address is usually a value object in an order domain: an order needs a shipping address snapshot, and two identical addresses are interchangeable. But classification is contextual. In a municipal registry that tracks occupancy history, inspections, or ownership for a particular property over time, an address or property record could become an entity.
This leads to an important interview answer:
“I would model
Addressas a value object here because the order needs an immutable delivery snapshot. If the requirements later add an address lifecycle, audit history, or independent lookup and updates, I would promote the relevant record to an entity.”
That is more precise than claiming that an address is always one category.
Entities vs Value Objects: Which one is BETTER?
Watch “Entities vs Value Objects: Which one is BETTER?” by Marco Lenzo for a concise visual explanation of identity equality, value equality, immutability, and context-dependent modeling.
Watch entity identity for the idea that an entity remains the same despite attribute changes. Continue with value equality, focusing on why equal value objects are interchangeable and normally immutable. Then skip to context matters for the important qualification that a concept’s classification follows requirements, not its everyday name.
Services: meaningful domain verbs without a natural owner
A domain service contains domain logic that does not fit naturally inside a single entity or value object. It is normally stateless: it does not hold its own evolving business lifecycle in the way a ticket or reservation does.
Do not create a service just because a method is longer than a few lines. First try to place behavior where the relevant state lives.
For example:
ticket.close(exit_time)belongs onParkingTicket.spot.occupy(ticket_id)belongs onParkingSpot.lot.available_spots_for(vehicle_type)can naturally belong onParkingLotif the lot owns and manages its spots.
A domain service becomes plausible when the operation is a domain-significant action involving multiple independent concepts and no single object is an honest owner. For example, a SpotAllocationService may select the best compatible spot from a collection according to accessibility, reservation, and distance rules that span the lot’s objects.
def select_best_spot(vehicle_type: str, spots: list["ParkingSpot"]) -> "ParkingSpot":
compatible = [
spot
for spot in spots
if spot.is_available_for(vehicle_type)
]
if not compatible:
raise NoCompatibleSpotError(vehicle_type)
return min(compatible, key=lambda spot: spot.distance_from_entrance)
In Python, a domain service can often be a well-named function rather than a class. A class is warranted only when it has cohesive collaborators or configuration that genuinely belong together.
Be careful not to mix up domain services and application services:
| Type | Main responsibility | Parking example |
|---|---|---|
| Entity | Maintains its own state and local rules | ParkingTicket.close() rejects a second close. |
| Value object | Represents an immutable value and its value-level behavior | Money.add() checks currency compatibility. |
| Domain service | Encapsulates business logic spanning domain objects with no natural owner | Selects the best valid spot across candidate spots. |
| Application service | Orchestrates a use case and infrastructure concerns | Loads a lot, invokes domain logic, saves changes, returns an API response. |
| Infrastructure component | Talks to external technology | TicketRepository, database client, payment gateway adapter. |
An application service might coordinate a “park vehicle” request: load the ParkingLot, invoke its allocation behavior or a domain service, persist the resulting ticket, and return a response. It should not become the permanent home of core rules such as “a closed ticket cannot be closed again.”

The diagram is a useful orientation, but do not infer that every use case requires a domain service. Often an entity method is clearer. For a simple ticket-closing operation, placing the rule on ParkingTicket is usually better than inventing TicketClosingService.
Use Tactical DDD to Design Microservices - Azure Architecture Center
Return to the Azure Architecture Center article for its distinction between domain services and application services. This distinction prevents a common LLD interview mistake: putting every business operation into a generic service layer.
In the “Domain and application services” subsection, read the service distinction. Pay particular attention to the boundary: domain services contain cross-entity business rules, while application services coordinate use cases, transactions, repositories, authentication, and external notifications.
A classification pass for an LLD interview
After requirements clarification, make a quick domain-concept pass. Do not turn every noun into a class. Words such as “availability,” “payment,” “entry,” and “notification” may denote a query, an operation, an external integration, a state transition, or an event rather than a new entity.
Use this sequence for each candidate concept:
-
State the role in the use case.
Is it something the system tracks, a descriptive value, or a business operation? -
Test for persistent identity.
Ask whether the business needs to find this exact instance later, distinguish it from otherwise identical instances, or preserve it through changes. -
Test equality and replacement.
If two instances with the same fields are interchangeable, it is likely a value object. If changing a field means a new value should replace the old one, that reinforces the choice. -
Find the natural home of behavior.
Put state-specific rules on the entity or value object that owns the relevant data. Consider a service only if the operation has no natural owner. -
Name the uncertainty explicitly.
If classification depends on a requirement, state your assumption rather than pretending there is one universal answer.
For the parking-lot example, a concise interview model could be:
| Classification | Concepts | Reasoning |
|---|---|---|
| Entities | ParkingLot, ParkingSpot, ParkingTicket | Each is independently identifiable and participates in a lifecycle. |
| Value objects | Money, LicensePlate, ParkingDuration, SpotLocation | Their attribute values define their meaning; replacement is clearer than mutation. |
| Enum or primitive initially | VehicleType, TicketStatus | A dedicated object adds little until rules or data make it necessary. |
| Domain service, if required | SpotAllocationService | Only if assignment rules span objects and do not belong cleanly to ParkingLot. |
| Application service | ParkVehicleUseCase | Coordinates request handling, persistence, and domain calls without owning core state rules. |
A strong verbal transition into a class diagram sounds like this:
“Based on the requirements, I will model the lot, spots, and tickets as entities because each has persistent identity and changing state. I will model money, plate, and location details as immutable value objects. The ticket will own its close transition, while I will keep allocation logic with the lot unless the selection rule grows into cross-object business logic that merits a domain service.”
This says what you chose, why you chose it, and where you deliberately avoided premature abstraction.
Common misclassifications
“Every database table is an entity”
No. A join table, audit record, cache entry, or persistence representation does not automatically become a domain entity. Model concepts from business behavior and lifecycle, not storage shape.
“Anything with an ID is an entity”
An ID-like value can itself be a value object. TicketId("T-100") may be an immutable value used to identify the ParkingTicket entity; the identifier is not the ticket.
“Value objects have no logic”
They can and should protect value-level rules. Money, DateRange, and Coordinates can validate construction and expose operations that return new values without side effects.
“Put all rules in services”
This usually produces vague classes such as TicketManager, ParkingManager, or OrderService. Prefer behavior on the object whose state the rule protects. A service is the exception for an operation with no natural entity or value-object owner.
“The classification is permanent”
It is not. A value object may become an entity when requirements add independent history, auditing, or lifecycle. An entity might be over-modeled if no one needs to distinguish its instances over time.
You should now be able to classify core concepts in an LLD prompt by asking whether they have persistent identity, value-based equality, or cross-object behavior without a natural owner. Entities track identity and lifecycle; value objects represent immutable, interchangeable values; domain services express important domain operations that belong to neither.
Next, we will turn the business rules from the requirements sheet into invariants: conditions your entities and value objects must preserve so the model cannot enter an invalid state.
Can't find a good explanation? Sign up and we'll make it for you
Sign up