Browser docs

Domain Model

Intent

Domain model pattern provides an object-oriented way of dealing with complicated logic. Instead of having one procedure that handles all business logic for a user action there are multiple objects and each of them handles a slice of domain logic that is relevant to it.

Explanation

Real world example

Let’s assume that we need to build an e-commerce web application. While analyzing requirements you will notice that there are few nouns you talk about repeatedly. It’s your Customer, and a Product the customer looks for. These two are your domain-specific classes and each of that classes will include some business logic specific to its domain.

In plain words

The Domain Model is an object model of the domain that incorporates both behavior and data.

Programmatic Example

In the example of the e-commerce app, we need to deal with the domain logic of customers who want to buy products and return them if they want. We can use the domain model pattern and create classes Customer and Product where every single instance of that class incorporates both behavior and data and represents only one record in the underlying table.

Here is the Product domain class with fields name, price, expirationDate which is specific for each product, productDao for working with DB, save method for saving product and getSalePrice method which return price for this product with discount.

 1@Slf4j
 2@Getter
 3@Setter
 4@Builder
 5@AllArgsConstructor
 6public class Product {
 7
 8    private static final int DAYS_UNTIL_EXPIRATION_WHEN_DISCOUNT_ACTIVE = 4;
 9    private static final double DISCOUNT_RATE = 0.2;
10
11    @NonNull private final ProductDao productDao;
12    @NonNull private String name;
13    @NonNull private Money price;
14    @NonNull private LocalDate expirationDate;
15
16    /**
17     * Save product or update if product already exist.
18     */
19    public void save() {
20        try {
21            Optional<Product> product = productDao.findByName(name);
22            if (product.isPresent()) {
23                productDao.update(this);
24            } else {
25                productDao.save(this);
26            }
27        } catch (SQLException ex) {
28            LOGGER.error(ex.getMessage());
29        }
30    }
31
32    /**
33     * Calculate sale price of product with discount.
34     */
35    public Money getSalePrice() {
36        return price.minus(calculateDiscount());
37    }
38
39    private Money calculateDiscount() {
40        if (ChronoUnit.DAYS.between(LocalDate.now(), expirationDate)
41                < DAYS_UNTIL_EXPIRATION_WHEN_DISCOUNT_ACTIVE) {
42
43            return price.multipliedBy(DISCOUNT_RATE, RoundingMode.DOWN);
44        }
45
46        return Money.zero(USD);
47    }
48}

Here is the Customer domain class with fields name, money which is specific for each customer, customerDao for working with DB, save for saving customer, buyProduct which add a product to purchases and withdraw money, returnProduct which remove product from purchases and return money, showPurchases and showBalance methods for printing customer’s purchases and money balance.

  1@Slf4j
  2@Getter
  3@Setter
  4@Builder
  5public class Customer {
  6
  7    @NonNull private final CustomerDao customerDao;
  8    @Builder.Default private List<Product> purchases = new ArrayList<>();
  9    @NonNull private String name;
 10    @NonNull private Money money;
 11
 12    /**
 13     * Save customer or update if customer already exist.
 14     */
 15    public void save() {
 16        try {
 17            Optional<Customer> customer = customerDao.findByName(name);
 18            if (customer.isPresent()) {
 19                customerDao.update(this);
 20            } else {
 21                customerDao.save(this);
 22            }
 23        } catch (SQLException ex) {
 24            LOGGER.error(ex.getMessage());
 25        }
 26    }
 27
 28    /**
 29     * Add product to purchases, save to db and withdraw money.
 30     *
 31     * @param product to buy.
 32     */
 33    public void buyProduct(Product product) {
 34        LOGGER.info(
 35                String.format(
 36                        "%s want to buy %s($%.2f)...",
 37                        name, product.getName(), product.getSalePrice().getAmount()));
 38        try {
 39            withdraw(product.getSalePrice());
 40        } catch (IllegalArgumentException ex) {
 41            LOGGER.error(ex.getMessage());
 42            return;
 43        }
 44        try {
 45            customerDao.addProduct(product, this);
 46            purchases.add(product);
 47            LOGGER.info(String.format("%s bought %s!", name, product.getName()));
 48        } catch (SQLException exception) {
 49            receiveMoney(product.getSalePrice());
 50            LOGGER.error(exception.getMessage());
 51        }
 52    }
 53
 54    /**
 55     * Remove product from purchases, delete from db and return money.
 56     *
 57     * @param product to return.
 58     */
 59    public void returnProduct(Product product) {
 60        LOGGER.info(
 61                String.format(
 62                        "%s want to return %s($%.2f)...",
 63                        name, product.getName(), product.getSalePrice().getAmount()));
 64        if (purchases.contains(product)) {
 65            try {
 66                customerDao.deleteProduct(product, this);
 67                purchases.remove(product);
 68                receiveMoney(product.getSalePrice());
 69                LOGGER.info(String.format("%s returned %s!", name, product.getName()));
 70            } catch (SQLException ex) {
 71                LOGGER.error(ex.getMessage());
 72            }
 73        } else {
 74            LOGGER.error(String.format("%s didn't buy %s...", name, product.getName()));
 75        }
 76    }
 77
 78    /**
 79     * Print customer's purchases.
 80     */
 81    public void showPurchases() {
 82        Optional<String> purchasesToShow =
 83                purchases.stream()
 84                        .map(p -> p.getName() + " - $" + p.getSalePrice().getAmount())
 85                        .reduce((p1, p2) -> p1 + ", " + p2);
 86
 87        if (purchasesToShow.isPresent()) {
 88            LOGGER.info(name + " bought: " + purchasesToShow.get());
 89        } else {
 90            LOGGER.info(name + " didn't bought anything");
 91        }
 92    }
 93
 94    /**
 95     * Print customer's money balance.
 96     */
 97    public void showBalance() {
 98        LOGGER.info(name + " balance: " + money);
 99    }
100
101    private void withdraw(Money amount) throws IllegalArgumentException {
102        if (money.compareTo(amount) < 0) {
103            throw new IllegalArgumentException("Not enough money!");
104        }
105        money = money.minus(amount);
106    }
107
108    private void receiveMoney(Money amount) {
109        money = money.plus(amount);
110    }
111}

In the class App, we create a new instance of class Customer which represents customer Tom and handle data and actions of that customer and creating three products that Tom wants to buy.

 1// Create data source and create the customers, products and purchases tables
 2final var dataSource = createDataSource();
 3deleteSchema(dataSource);
 4createSchema(dataSource);
 5
 6// create customer
 7var customerDao = new CustomerDaoImpl(dataSource);
 8
 9var tom =
10    Customer.builder()
11        .name("Tom")
12        .money(Money.of(USD, 30))
13        .customerDao(customerDao)
14        .build();
15
16tom.save();
17
18// create products
19var productDao = new ProductDaoImpl(dataSource);
20
21var eggs =
22    Product.builder()
23        .name("Eggs")
24        .price(Money.of(USD, 10.0))
25        .expirationDate(LocalDate.now().plusDays(7))
26        .productDao(productDao)
27        .build();
28
29var butter =
30    Product.builder()
31        .name("Butter")
32        .price(Money.of(USD, 20.00))
33        .expirationDate(LocalDate.now().plusDays(9))
34        .productDao(productDao)
35        .build();
36
37var cheese =
38    Product.builder()
39        .name("Cheese")
40        .price(Money.of(USD, 25.0))
41        .expirationDate(LocalDate.now().plusDays(2))
42        .productDao(productDao)
43        .build();
44
45eggs.save();
46butter.save();
47cheese.save();
48
49// show money balance of customer after each purchase
50tom.showBalance();
51tom.showPurchases();
52
53// buy eggs
54tom.buyProduct(eggs);
55tom.showBalance();
56
57// buy butter
58tom.buyProduct(butter);
59tom.showBalance();
60
61// trying to buy cheese, but receive a refusal
62// because he didn't have enough money
63tom.buyProduct(cheese);
64tom.showBalance();
65
66// return butter and get money back
67tom.returnProduct(butter);
68tom.showBalance();
69
70// Tom can buy cheese now because he has enough money
71// and there is a discount on cheese because it expires in 2 days
72tom.buyProduct(cheese);
73
74tom.save();
75
76// show money balance and purchases after shopping
77tom.showBalance();
78tom.showPurchases();

The program output:

 117:52:28.690 [main] INFO com.iluwatar.domainmodel.Customer - Tom balance: USD 30.00
 217:52:28.695 [main] INFO com.iluwatar.domainmodel.Customer - Tom didn't bought anything
 317:52:28.699 [main] INFO com.iluwatar.domainmodel.Customer - Tom want to buy Eggs($10.00)...
 417:52:28.705 [main] INFO com.iluwatar.domainmodel.Customer - Tom bought Eggs!
 517:52:28.705 [main] INFO com.iluwatar.domainmodel.Customer - Tom balance: USD 20.00
 617:52:28.705 [main] INFO com.iluwatar.domainmodel.Customer - Tom want to buy Butter($20.00)...
 717:52:28.712 [main] INFO com.iluwatar.domainmodel.Customer - Tom bought Butter!
 817:52:28.712 [main] INFO com.iluwatar.domainmodel.Customer - Tom balance: USD 0.00
 917:52:28.712 [main] INFO com.iluwatar.domainmodel.Customer - Tom want to buy Cheese($20.00)...
1017:52:28.712 [main] ERROR com.iluwatar.domainmodel.Customer - Not enough money!
1117:52:28.712 [main] INFO com.iluwatar.domainmodel.Customer - Tom balance: USD 0.00
1217:52:28.712 [main] INFO com.iluwatar.domainmodel.Customer - Tom want to return Butter($20.00)...
1317:52:28.721 [main] INFO com.iluwatar.domainmodel.Customer - Tom returned Butter!
1417:52:28.721 [main] INFO com.iluwatar.domainmodel.Customer - Tom balance: USD 20.00
1517:52:28.721 [main] INFO com.iluwatar.domainmodel.Customer - Tom want to buy Cheese($20.00)...
1617:52:28.726 [main] INFO com.iluwatar.domainmodel.Customer - Tom bought Cheese!
1717:52:28.737 [main] INFO com.iluwatar.domainmodel.Customer - Tom balance: USD 0.00
1817:52:28.738 [main] INFO com.iluwatar.domainmodel.Customer - Tom bought: Eggs - $10.00, Cheese - $20.00

Class diagram

Applicability

Use a Domain model pattern when your domain logic is complex and that complexity can rapidly grow because this pattern handles increasing complexity very well. Otherwise, it’s a more complex solution for organizing domain logic, so shouldn’t use Domain Model pattern for systems with simple domain logic, because the cost of understanding it and complexity of data source exceeds the benefit of this pattern.

Credits