skip to content

In an order-entry system a controller receives "add product P with quantity 3 to order O". Applying the GRASP "Creator" heuristic, which class should construct the new order-line object, and what concretely goes wrong if the controller constructs it instead?

level: middleimportance: should knowfreq 40%

answer

  1. Order aggregates lines → Order creates them
  2. addLine(product, qty), not getLines().add(...)
  3. exposed collection = no invariants
  4. totals/duplicates/status checks in one path
  5. service resolves Product, entity builds the line

basics

~20 s

The Order should build the line, because it contains and owns its lines and holds the collection they join. If the controller builds it, the controller must know the line type, the order must expose its internal list, and totals or duplicate rules can silently break.

solid answer

~50 s

Creator assigns instantiation to the class that aggregates, contains, records, closely uses, or holds the initializing data for the created type. `Order` satisfies the strongest criteria: it aggregates order lines (they have no life outside the order) and owns the collection. So the API becomes `order.addLine(product, quantity)`, with the line constructed inside. Consequences of doing it in the controller: (1) the controller gains a compile-time dependency on the line type, widening the presentation layer's knowledge of the domain; (2) `Order` must expose a mutable collection (`getLines().add(...)`), destroying encapsulation; (3) invariants — recomputing totals, merging duplicate product lines, rejecting quantity ≤ 0, checking the order is still editable — must be duplicated by every caller and will eventually be forgotten; (4) tests must construct lines by hand, so the real construction path is under-tested. Keeping creation inside the owner turns those invariants into a single enforced code path.

code

pseudocode · 21 lines
pseudocode
// Creator-respecting
class Order {
  private lines = []
  private status

  addLine(product, quantity) {
    require(status == DRAFT, "order not editable")
    require(quantity >= 1)
    existing = lines.find(l -> l.product == product)
    if (existing != null) { existing.increase(quantity); }
    else { lines.add(new OrderLine(product, quantity, product.currentPrice())) }
    recalculateTotal()
  }
  lines() = readOnlyView(lines)
}

// Controller
order   = orders.byId(orderId)
product = products.byId(productId)
order.addLine(product, 3)     // no knowledge of OrderLine at all
orders.save(order)

go deeper

for a junior

Say Order creates the line because it holds the lines, and note that otherwise the list has to be exposed.

for a middle

Name the Creator criteria that Order satisfies and list at least three concrete consequences (coupling, exposed collection, lost invariants).

for a senior

Add the invariant-boundary framing, where the repository/product lookup belongs, and the factory escape hatch for varying line types.

for a principal

Discuss the aggregate root as the sole creation entry point, reconstitution vs creation in persistence, and how creation placement shapes change amplification across layers and teams.

## The scenario, spelled out Domain objects: - `Order` — an order under construction; holds a list of lines, a status, and a total. - `OrderLine` — a product reference, a quantity, and a captured unit price. - `Product` — an independently existing catalogue item. - `OrderController` (or an application service) — receives the request `addProduct(orderId, productId, qty)`. ## Applying the Creator criteria Recall the criteria: assign creation of `A` to `B` if `B` **aggregates**, **contains**, **records**, **closely uses** `A`, or **has the initializing data** for `A`. | Candidate | Criteria satisfied | Verdict | |---|---|---| | `Order` | aggregates + contains lines; holds the collection they must join; knows the order's currency/discount state | **Creator** | | `Product` | is referenced by a line, but does not own lines | no | | `OrderController` | "closely uses" at best; has raw request data only | weak | | `OrderRepository` | records orders, not lines | no | `Order` wins on the strongest criterion (aggregation/ownership of lifetime). The resulting API: ``` order.addLine(product, quantity) // Order constructs OrderLine internally ``` The controller's job is only: load the order, load the product, call the method, save. ## What concretely breaks if the controller constructs the line ### 1. Coupling widens `OrderController` now imports `OrderLine` and knows its constructor signature. Every constructor change (adding captured price, tax code, line number) ripples into presentation code — and into every other caller that learned to build lines. ### 2. Encapsulation is destroyed To attach the line, `Order` must publish its collection: `order.getLines().add(line)`. That exposed collection is now mutable by anyone: lines can be removed, reordered, or added to a *shipped* order. The class can no longer make any promise about its own contents. ### 3. Invariants leak and rot Realistic rules attached to adding a line: - the order must be in an editable status (not `SHIPPED`/`CANCELLED`); - quantity must be ≥ 1; - adding an existing product merges quantities rather than creating a duplicate line; - the line captures the *current* unit price, not a live reference to the product's mutable price; - the order total and line numbering are recomputed. When `Order` is the Creator, these live in one method that cannot be bypassed. When the controller creates the line, each rule must be re-implemented at every call site — the import path, an admin screen, a bulk API, a test fixture. In practice one of them forgets, and you get orders whose total does not equal the sum of their lines. ### 4. Testability degrades Unit tests build `OrderLine` directly and assert on it, so the *real* creation path (with its validation) is never exercised. Bugs hide in the untested branch. ### 5. Change amplification Introducing line-level discounts means touching the controller, the batch importer, the test fixtures — instead of one method. ## Nuances worth raising in an interview - **Who resolves the `Product`?** Not `Order`. Loading by id is a repository concern; the application service resolves `Product` and hands the *object* to `order.addLine(product, qty)`. Creator does not mean the aggregate reaches into infrastructure. - **Ambient data (timestamp, user, generated id).** Pass it in as a parameter (or a small context/parameter object) rather than injecting a clock or id generator into the entity. - **Return value.** Returning the created line is fine, but returning it as an immutable view (or returning nothing and letting the caller re-read) keeps the invariant boundary tight. - **When to override Creator here.** If line construction genuinely varies — a tax strategy per jurisdiction, a promotional line subtype chosen by rules engine — extract an `OrderLineFactory` (a Pure Fabrication) and let `Order` call it. `Order` still *owns* the line; it merely delegates the messy selection. - **Persistence reconstitution.** The ORM will rebuild lines by reflection, bypassing `addLine`. That is fine — reconstitution is not creation — but keep that constructor non-public so no application code uses it as a shortcut around the invariants. ## Summary rule Creation belongs where the invariants belong. If the owner cannot see the object being born, it cannot promise anything about the object's existence.

  • The order line needs the product's price at the time of adding. Does that change who the Creator is?
    No. It reinforces it: Order can ask the passed-in Product for its current price and capture it, keeping the price-snapshot rule in one place. The controller would otherwise have to know that prices must be snapshotted.
  • What if creating the line requires a tax-rate lookup from an external service?
    Don't inject the service into the entity. Either resolve the tax rate in the application layer and pass it as a parameter, or extract an OrderLineFactory (Pure Fabrication) holding that collaborator, which Order or the service calls. Ownership of the line stays with Order.
  • Your ORM constructs OrderLine directly when loading from the database — does that break the design?
    No. That is reconstitution of an already-valid object, not creation of a new one. Keep the reconstitution constructor non-public/framework-only so application code cannot use it to bypass addLine's validation.

context