Domain-driven design with Java EE 6

An object-oriented architecture for interactive applications

For Java EE applications based on type-dependent algorithms, a domain-driven design that leverages object-orientation is better than a procedurally implemented service-oriented architecture. Adam Bien explains how and why object-oriented persistence makes your domain-driven application's code more efficient, maintainable, and testable. Level: Advanced

In "Lean service architectures with Java EE 6," I discussed how to implement a maintainable service-oriented architecture (SOA) that is based on anemic persistent objects and transient, stateless services. In this follow-up article, you'll learn about an architectural approach that is the exact opposite of that. What ties together the two architectures is that both can be implemented with Java EE 6 and, in particular, Enterprise JavaBeans (EJB).

As I'll explain in this article, you can build domain-driven applications with "real" objects -- objects that comprise encapsulated state and well-defined behavior. Whereas SOA is stateless and tries to hide a component's implementation, domain-driven applications are mostly stateful and aim to expose the domain objects to the user as directly and conveniently as possible.

Procedural vs domain-driven design

The vast majority of J2EE and Java EE applications are built in a procedural way. The business logic is decomposed into tasks and resources, which map, respectively, to services and anemic, persistent entities. Such an approach works surprisingly well until type-specific behavior for domain objects must be realized. Then the lack of object orientation in the persistence layer can cause problems.

Attempts to implement object-oriented algorithms with procedural techniques end up in many instanceof checks and/or lengthy if-statements. Such type checks are required because in the SOA world, domain objects are anemic, so inheritance doesn't pay off. Even if inheritance is used to design the domain model, object orientation's most powerful feature -- polymorphic behavior -- isn't leveraged at all.

The anemic domain model forces you to implement type checks outside the anemic entities, in services or service facades. To make their internal state accessible to the services and service facades, the anemic structures (technically they are not objects, by definition) must expose their internal state to the outside world too -- through field accessors (getters/setters) -- requiring a lot of plumbing.

In data-driven use cases such as master data management (CRUD), you can implement workflow-related, procedural behavior efficiently with service facades and few anemic entities. A domain-driven architecture, on the other hand, is perfect for more-complex, type-dependent algorithms in more-interactive applications. Most nontrivial Java EE projects require both approaches.

Essential ingredients of domain-driven design

The crucial artifact in any domain-driven application is a domain object. A domain object can be considered a real-world artifact in the given project context. The challenge is to grasp the context and not try to model a perfect, theoretical world. Pragmatism rules. Domain objects are just cohesive classes with encapsulated state and related behavior. They use inheritance naturally, where appropriate.

Domain objects represent important concepts in the target domain and so must often be persistent. The Java Persistence API (JPA) turns out to be quite flexible for mapping rich domain objects to relational tables. The more complex the logic you need to realize, the more easily object-oriented persistence can be maintained and developed.

The real issue with complex logic realized with anemic structures are type distinctions in the service layer. Massive if-statements might be necessary to differentiate among entity types. Every introduction of a new subclass, or even a change in the existing business logic, requires you to find, enhance, and test these type checks. For example, the computation of shipping costs dependent on weight and the OrderItem type would look like Listing 1.

Listing 1. Procedural approach to switching between entity types

@Stateless
public class ShipmentService {
  public final static int BASIC_COST = 5;

  @PersistenceContext
  private EntityManager em;

     public int getShippingCosts(int loadId) {
        Load load = em.find(Load.class, loadId);
        return computeShippingCost(load);
    }

    int computeShippingCost(Load load){
        int shippingCosts = 0;
        int weight = 0;
        int defaultCost = 0;
        for (OrderItem orderItem : load.getOrderItems()) {
            LoadType loadType = orderItem.getLoadType();
            weight = orderItem.getWeight();
            defaultCost = weight * 5;
            switch (loadType) {
                case BULKY:
                    shippingCosts += (defaultCost + 5);
                    break;
                case LIGHTWEIGHT:
                    shippingCosts += (defaultCost - 1);
                    break;
                case STANDARD:
                    shippingCosts += (defaultCost);
                    break;
                default:
               throw new IllegalStateException("Unknown type: " + loadType);
            }
        }
        return shippingCosts;
    }
}

In Listing 1, all the logic resides in a service or service facade and consists mainly of type checks. The service must differentiate among the different types to apply type-dependent computational algorithms.

The same logic can be expressed with only a fraction of code in an object-oriented way, as shown in Listing 2. You can even eliminate the service, because the logic is implemented in the domain object itself.

Listing 2. Polymorphic business logic inside a domain object, without type checks

@Entity
public class Load {

    @OneToMany(cascade = CascadeType.ALL)
    private List<OrderItem> orderItems;
    @Id
    private Long id;

    protected Load() {
        this.orderItems = new ArrayList<OrderItem>();
    }

    public int getShippingCosts() {
        int shippingCosts = 0;
        for (OrderItem orderItem : orderItems) {
            shippingCosts += orderItem.getShippingCost();
        }
        return shippingCosts;
    }
//...
}

In Listing 2, computation of the total costs is realized inside a rich domain object -- the persistent domain object (PDO) -- not inside a service. Object-oriented persistence makes the code more concise and so also easier to understand. Actual computation of type-specific costs is performed in concrete subclasses such as the BulkyItem class in Listing 3, which makes testing the computation outside the EntityManager much easier.

Listing 3. Type-specific computation in a subclass

@Entity
public class BulkyItem extends OrderItem{

    public BulkyItem() {
    }

    public BulkyItem(int weight) {
        super(weight);
    }

    @Override
    public int getShippingCost() {
        return super.getShippingCost() + 5;
    }
}

We can change the computation of the shipping cost of a BulkyItem without touching the remaining classes. And it's easy to introduce a new subclass without affecting the computation of the total costs in the Load class.

The creation of a PDO graph is easier and more intuitive, compared to the traditional getter/setter-driven approach shown in Listing 4.

Listing 4. The procedural way of object construction

Load load = new Load();
OrderItem standard = new OrderItem();
standard.setLoadType(LoadType.STANDARD);
standard.setWeight(5);
load.getOrderItems().add(standard);
OrderItem light = new OrderItem();
light.setLoadType(LoadType.LIGHTWEIGHT);
light.setWeight(1);
load.getOrderItems().add(light);
OrderItem bulky = new OrderItem();
bulky.setLoadType(LoadType.BULKY);
bulky.setWeight(1);
load.getOrderItems().add(bulky);

Instead of creating the anemic objects independently and wiring them together afterwards, you can take a concise object-oriented approach using the Builder pattern, as shown in Listing 5.

Listing 5. Concise PDO construction with the Builder pattern

Load build = new Load.Builder().
    withStandardItem(5).
    withLightweightItem(1).
    withBulkyItem(1).
    build();

The Builder pattern lets us construct the Load domain object with concrete OrderItems in convenient way. Method chaining saves some superfluous lines of code and allows autocompletion support in common IDEs. In addition, consistency checks are performed in the build() method, so it's not necessary to create empty Load objects.

Builder is implemented as a static inner class, as shown in Listing 6.

Listing 6. Builder realized as a static inner class inside a PDO

@Entity
public class Load {

    @OneToMany(cascade = CascadeType.ALL)
    private List<OrderItem> orderItems;
    @Id
    private Long id;

    protected Load() {
        this.orderItems = new ArrayList<OrderItem>();
    }

    public static class Builder {

        private Load load;

        public Builder() {
            this.load = new Load();
        }

        public Builder withBulkyItem(int weight) {
            this.load.add(new BulkyItem(weight));
            return this;
        }

        public Builder withStandardItem(int weight) {
            this.load.add(new StandardItem(weight));
            return this;
        }

        public Builder withLightweightItem(int weight) {
            this.load.add(new LightweightItem(weight));
            return this;
        }

        public Load build() {
            if (load.orderItems.size() == 0) {
                throw new IllegalStateException("...");
            }
            return this.load;
        }
    }

    void add(OrderItem item) {
        this.orderItems.add(item);

    }

    public int getShippingCosts() {
        int shippingCosts = 0;
        for (OrderItem orderItem : orderItems) {
            shippingCosts += orderItem.getShippingCost();
        }
        return shippingCosts;
    }

    public OrderItem lightest(){
      if(this.orderItems == null || this.orderItems.size() == 0)
            return null;
       Collections.sort(this.orderItems, new WeightComparator());
       return this.orderItems.iterator().next();
    }

    public OrderItem heaviest(){
      if(this.orderItems == null || this.orderItems.size() == 0)
            return null;
       Collections.sort(this.orderItems, new WeightComparator());
       Collections.reverse(orderItems);
       return this.orderItems.iterator().next();
    }

    public OrderItem dropHeaviest(){
        OrderItem heaviest = heaviest();
        if(heaviest != null)
            drop(heaviest);
        return heaviest;
    }

    public OrderItem dropLightest(){
        OrderItem lightest = lightest();
        if(lightest != null)
            drop(lightest);
        return lightest;
    }

    public Load drop(OrderItem orderItem){
        this.orderItems.remove(orderItem);
        return this;
    }
    public Long getId() {
        return id;
    }

    public int getNumberOfOrderItems(){
        return this.orderItems.size();
    }
}

The Builder class creates Load and provides methods with fluent names (such as withBulkyItem). Each method creates a corresponding subclass of OrderItem, adds it to the load, and returns the Builder instance itself. This lets us construct the Load with convenient method chaining.

The logic I've shown you so far is limited to the construction and computation of shipping costs. Even more interesting is the implementation of non-idempotent methods. The dropHeaviest() method removes the heaviest OrderItem from the Load. There's nothing exciting about that, but keep in mind that the PDOs are persistent entities. Every change to the state, and even to relations, is automatically synchronized with the persistence at the end of the transaction. Just imagine an object-oriented implementation of a Subversion (SVN) repository with JPA. A change to a versioned file must be propagated to all packages until the project root is reached. The whole path is marked as changed. A SVN commit at the project level causes a recursive descent through all changed folders until all changed files are committed as well. To implement such a cascading algorithm in object-oriented way would be straightforward, whereas a procedural approach would be error-prone and hard to maintain.

Testing PDOs

PDOs are just annotated classes, so you can test them just like usual POJOs. For the business-logic tests you don't even need to mock out or bootstrap the EntityManager. You can easily create the instances needed to perform the test in the @Before or @BeforeClass methods and use them in the actual test methods, as shown in Listing 7.

1 2 3 Page
Recommended
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more