Welcome to tactical design
Strategic design told you where Order lives — the Sales Bounded Context, inside the core domain. Tactical design is about what Order actually is as code, and the most basic question you’ll ask of every class in a model is this one: does it have a meaningful identity that must be tracked through change, or is it defined entirely by its attributes? Get that one question right for every class and the rest of tactical design — Aggregates, Repositories, Services — mostly falls out of it.
Entity: identity survives change
An Entity is a domain object defined by a continuous identity that runs through its entire lifecycle, not by its current attribute values. Two Entities with identical attributes are still two different things if their identities differ — and the same Entity is still “the same one” even after every attribute on it has changed. Order is the textbook case: an order placed on Monday, in PLACED status with two lines, is still the same order on Friday when it’s SHIPPED with a tracking number attached. Nothing about its attributes is the same, but it’s unmistakably one continuous thing.
Characteristics:
- A distinct identity — an id — set once at creation and never changed.
- Mutable: its state evolves through domain operations over its lifecycle.
- Equality by identity: two
Orderinstances are equal if theirOrderIds match, full stop, regardless of what their other fields say. - A lifecycle: created, modified through a sequence of valid states, eventually closed.
final class Order {
private final OrderId id;
private OrderStatus status;
private final List<OrderLine> lines;
void cancel() {
if (status == OrderStatus.SHIPPED) {
throw new IllegalStateException("Cannot cancel a shipped order");
}
this.status = OrderStatus.CANCELLED;
}
@Override
public boolean equals(Object o) {
return o instanceof Order other && this.id.equals(other.id);
}
@Override
public int hashCode() { return id.hashCode(); }
}
Code signals in Java: a field like id, orderId, or a dedicated identity Value Object such as OrderId, often annotated @Id; equals()/hashCode() implemented over the id field alone — a strong, almost decisive signal; mutating methods with domain names (order.cancel(), account.debit(amount)) rather than bare getters and setters. JPA’s @Entity annotation is a hint, not proof — plenty of @Entity-annotated classes are really persistence rows for what is, conceptually, a Value Object.
Value Object: defined entirely by attributes
A Value Object describes a characteristic or a measurement and has no identity at all — two Value Objects with equal attributes are completely interchangeable. Money is the canonical example: a Money representing $19.99 is indistinguishable from, and interchangeable with, any other Money representing $19.99. There’s no “this particular dollar amount” the way there’s “this particular order.”
Characteristics:
- Immutable — once created, it never changes; an operation that would “change” it returns a new instance instead.
- Equality by value —
equals()/hashCode()compare every field, not an id. - Often conceptually whole: it bundles attributes that only make sense together. A
ShippingAddressis street, city, and postcode as one unit — splitting it into three loose strings would lose the fact that they only mean something combined. - Frequently carries real domain logic of its own (
money.add(other),address.isInternational()).
record Money(BigDecimal amount, Currency currency) {
Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(this.amount.add(other.amount), this.currency);
}
}
record OrderLine(String isbn, int quantity, Money unitPrice) {
Money lineTotal() { return unitPrice.multipliedBy(quantity); }
}
Code signals in Java: final fields with no setters, populated only through a constructor or static factory; equals()/hashCode() over all fields; a Java record is about as strong a Value Object signal as Java gives you; JPA’s @Embeddable/@Embedded; methods that return a new instance instead of mutating (return new Money(...)); a type that exists purely to wrap a primitive with domain meaning, like an Isbn wrapping a String so it can’t be confused with an arbitrary string.
When in doubt: mutability plus id-based equality means Entity. Immutability plus full-value equality means Value Object. There's rarely a third option — most ambiguity resolves the moment you ask "would I ever need to look this specific one up again by an id, distinct from another with the same values?"
OrderLine: the case that trips people up
OrderLine is worth dwelling on because it’s the example most people misclassify on a first pass. It looks entity-shaped — it’s “a thing inside an order” — and some codebases even give it a database row with its own primary key, which feels like an id. But ask the decisive question: does anyone in the Sales context ever need to say “this specific line, as distinct from another line with the same ISBN, quantity, and price, even on the same order”? No. Two lines for the same book, same quantity, same price are completely interchangeable — if you replaced one with the other, nothing about the order’s meaning would change. That’s the Value Object signature, and it’s why OrderLine was written as a record above, with no id field at all. A database primary key is a persistence-layer convenience; it isn’t, by itself, evidence of a domain identity.
Note. This is exactly the trap the spec this course is built from calls out directly:
@Entity(the JPA annotation) is a persistence marker, not proof of a domain Entity. Classify by what the type does, not by what the framework calls it.
You now have the two basic building blocks. The next module puts them together: Order doesn’t float on its own — it owns a collection of OrderLines, enforces rules across all of them at once, and is the only thing outside code is allowed to reference directly. That cluster, and the boundary around it, is the Aggregate.