Programming

TDD: Immutable Order Static Factory vs Mutable Java

In test driven development, model 'place order' intent with static factory, private constructor, and OrderState enum. Advantages over mutable setters, trade-offs, DDD tips, and TDD code examples for Java design patterns.

1 answer 1 view

In Test-First Development I’m modeling “placing an order” as an intent. I have this unit test and a simple mutable Order implementation:

java
@Test
void placing_an_order_creates_only_an_intent() {
 Order order = new Order();
 assertThat(order).isNotNull();
 assertThat(order.isPlaced()).isFalse();
 order = order.place();
 assertThat(order.getState()).isEqualTo("REQUESTED");
 assertThat(order.isPlaced()).isTrue();
}
java
package com.foodvishwas.order.domain;

public class Order {

 private boolean placed;

 private String state;

 public boolean isPlaced() {
 return placed;
 }

 public void setPlaced(boolean placed) {
 this.placed = placed;
 }

 public String getState() {
 return state;
 }

 public void setState(String state) {
 this.state = state;
 }

 public Order place(){
 Order order = new Order();
 order.setState("REQUESTED");
 order.setPlaced(true);
 return order;
 }

}

Now I’m considering an alternative using a private constructor, a static factory method and an OrderState enum:

java
public class Order {

 private final OrderState state;

 // Private constructor
 private Order(OrderState state) {
 this.state = state;
 }

 // Public, named creation method
 public static Order place() {
 return new Order(OrderState.REQUESTED);
 }

 public OrderState getState() {
 return state;
 }
}

My questions:

  • If placing an order is just an intent, why is the mutable, setter-based approach with a public constructor not OK?
  • What are the advantages of the static factory + private constructor + OrderState enum (immutability, clearer intent, preserving invariants, API design, testability, etc.)?
  • Are there trade-offs or downsides to the static factory approach?
  • Is using an enum the best way to model order states, or are there better alternatives (value objects, sealed classes, state pattern)?

Which approach is preferable in TDD/DDD for modeling the “place” intent, and why?

In test driven development (TDD), model “place” as an explicit creation/transition and prefer an immutable Order built with a private constructor + static factory (or instance transition that returns a new immutable Order) so the “place” intent is expressed in the API, invariants are preserved and tests stay simple. The setter-based public-constructor approach allows half‑built, mutable Orders, which breaks invariants, makes tests brittle and hides intent; trade‑offs for the immutable/factory approach include minor boilerplate, some allocation/ORM friction, and the need for a different mapping strategy.


Contents


Why the mutable, setter-based approach is problematic

You can create an Order with new Order() and incrementally set fields. That looks convenient, but what exactly does new Order() mean in your domain? Is it a draft, an empty DTO, or a placed intent? The public constructor + setters pattern leaves that question unanswered.

Consequences:

  • Invariants are unenforced. Someone can call setState(null) or setPlaced(true) without going through domain rules. Tests then only pass because they depend on precise call order, not on the type guaranteeing correctness.
  • State can be inconsistent or half‑built. That makes reasoning harder and bugs more likely when code paths assume a valid, fully‑initialized object.
  • Tests become coupled to mutation rather than behavior. Mutability encourages sequences of changes; TDD encourages expressing intent and behavior first.
  • Concurrency and caching become risky: shared mutable objects require synchronization or defensive copies.

If you want the semantics “placing an order is an intent”, the API should make that intent explicit and safe. Immutable design patterns are a proven way to do that; see the practical immutability checklist and rationale in Baeldung’s guide on immutable objects: https://www.baeldung.com/java-immutable-object and the mutable vs immutable comparison: https://www.baeldung.com/java-mutable-vs-immutable-objects.


Advantages of private constructor + static factory + OrderState enum (TDD benefits)

Named creation + immutability does two jobs at once: it documents intent and enforces correctness.

Clearer intent and API design

  • A named factory like Order.place() or Order.draft() tells callers what they’re creating. No guesswork.
  • You avoid the noisy two-step new Order(); order.setState(...); pattern. Named factories are self‑documenting and read well in tests and code.

Preserving invariants and safety

  • Make fields final and initialize them in a private constructor so every instance is valid by construction. That prevents half‑baked objects.
  • The class becomes safe to share across threads with no synchronization.

Better testability and reasoning

  • Tests assert outcomes of pure transitions (input → output). You get deterministic assertions and easier refactoring.
  • You’ll find tests focus on behavior (what the domain does) rather than on how many setters got called.

Other practical benefits

Example advantages are discussed in Baeldung’s articles on immutability and design choices: https://www.baeldung.com/java-immutable-object and https://www.baeldung.com/java-mutable-vs-immutable-objects.


Trade‑offs and practical downsides

Nothing is free—here are the common trade‑offs you’ll run into.

Boilerplate and perceived verbosity

  • You may write a few more factory methods, small constructor checks and copy-style methods. It’s deliberate, not gratuitous.

Object churn and allocation

  • Immutable transitions return new objects. On hot code paths that can create more short‑lived objects. Modern JVMs handle this well, but extremely latency‑sensitive systems sometimes prefer in‑place mutation.

Persistence and framework friction

  • ORMs like JPA expect a no‑arg constructor and mutable fields. Immutable domain objects require either:
  • a protected no‑arg constructor for the ORM (breaking strict immutability for the persistence layer), or
  • a mapping layer (DTO/adapter) that translates between the immutable domain model and the persistence model. Baeldung discusses aggregate persistence trade‑offs: https://www.baeldung.com/spring-persisting-ddd-aggregates.

Discoverability and test doubles

  • Static factories hide constructors; some devs find this less discoverable at first. Mocking or reflection‑based libraries may require adapters or configuration.

So: prefer immutability for domain logic and accept small costs, or use an adapter pattern when persistence or framework constraints force mutation.


Java design patterns: enum vs value objects vs sealed classes vs State pattern

Which technique to use for Order states depends on whether the state is merely a label or whether it owns behavior.

Enum — simple, pragmatic

  • Use an enum when states are simple tags (DRAFT, REQUESTED, SHIPPED). Enums are compact, compare-by-identity, and easy to serialize. They’re perfect when state is data-only.

Value object — richer identity/data

  • If state needs associated data (timestamps, codes) keep it as an immutable value object (a small class or record) rather than stuffing everything into an enum.

Sealed classes / records (Java 17+)

  • When states need different data shapes or you want exhaustiveness checks, sealed types let you model an algebraic data type. That can be clearer than a large enum with external switch logic.

State pattern — when behavior varies per state

  • When “what an order does” differs by state, encapsulate behavior in state classes (the State pattern). That removes switch/case logic and puts behavior with the state implementation; see the pattern description and rationale: https://refactoring.guru/design-patterns/state.

Quick guideline

  • If you only query “isPlaced?” or check an ID, use an enum.
  • If each state implements distinct behavior (validateTransition, calculateFees, etc.), prefer State pattern or sealed types to keep logic local and testable.

Practical, TDD-friendly code and updated unit test

Below is a compact, TDD-friendly immutable approach that preserves your original test semantics (start unplaced, placing returns a new requested order). This keeps intent explicit and prevents accidental mutation.

java
// OrderState.java
public enum OrderState {
 DRAFT,
 REQUESTED;

 public boolean isPlaced() { return this == REQUESTED; }
}

// Order.java
public final class Order {
 private final OrderState state;

 private Order(OrderState state) {
 this.state = state;
 }

 // factory for the initial (not placed) order
 public static Order draft() {
 return new Order(OrderState.DRAFT);
 }

 // transition: returns a new Order representing the intent to place
 public Order place() {
 if (this.state != OrderState.DRAFT) {
 throw new IllegalStateException("Order cannot be placed from state: " + state);
 }
 return new Order(OrderState.REQUESTED);
 }

 public OrderState getState() { return state; }
 public boolean isPlaced() { return state.isPlaced(); }

 // equals/hashCode/toString omitted for brevity
}

Updated test (TDD style):

java
@Test
void placing_an_order_creates_only_an_intent() {
 Order order = Order.draft();
 assertThat(order).isNotNull();
 assertThat(order.isPlaced()).isFalse();

 Order requested = order.place();
 assertThat(requested.getState()).isEqualTo(OrderState.REQUESTED);
 assertThat(requested.isPlaced()).isTrue();

 // original draft remains unchanged
 assertThat(order.getState()).isEqualTo(OrderState.DRAFT);
}

Notes

  • If you prefer a “create-a-place-intent-from-scratch” API, you can add public static Order placeIntent() and skip the draft step; choose what matches the domain language in your ubiquitous language.

Which approach to pick in TDD/DDD and why?

Short answer: prefer the immutable factory/transition approach in TDD/DDD, unless external constraints force mutability.

Why prefer it?

  • It expresses the intent directly in the API (good for both TDD and the ubiquitous language in DDD). Tests then assert behavior and intent, not object plumbing.
  • It preserves invariants at construction time, making domain rules easier to enforce and refactor.
  • It simplifies reasoning about code and concurrency: an immutable Order is a small, safe, and explicit value.

When to compromise

  • If you must integrate tightly with JPA or similar frameworks, either:
  • use a persistence adapter or mapping layer so the domain remains immutable and your repository handles translation, or
  • provide a protected no‑arg constructor and package-private setters strictly for the ORM (document this as a persistence-only concession).
  • If states encapsulate different behavior, use the State pattern or sealed classes rather than a plain enum.

If you’re doing “test driven development by example”, start with the minimal test that expresses the intent (create a draft then place it, or create a place-intent directly), make it pass with a small immutable model, then refactor to add validation or richer behavior. That keeps tests readable and focused on domain intent rather than implementation.


Sources


Conclusion

In test driven development, model “place” as an explicit creation/transition using an immutable Order (private constructor + static factory and/or instance transition) with an OrderState enum when states are simple. That design preserves invariants, clarifies intent in tests, and reduces bugs—while the main trade‑offs are modest boilerplate and persistence mapping work. If order states carry behavior, evolve to sealed types or the State pattern so your design stays expressive and maintainable.

Authors
Verified by moderation
Moderation
TDD: Immutable Order Static Factory vs Mutable Java