├── .gitignore
├── LICENSE
├── README.md
├── invoicing
├── pom.xml
└── src
│ └── main
│ ├── java
│ └── net
│ │ └── pkhapps
│ │ └── ddd
│ │ └── invoicing
│ │ ├── InvoicingApp.java
│ │ ├── application
│ │ ├── InvoiceCreator.java
│ │ └── InvoiceService.java
│ │ ├── domain
│ │ └── model
│ │ │ ├── Invoice.java
│ │ │ ├── InvoiceId.java
│ │ │ ├── InvoiceItem.java
│ │ │ ├── InvoiceItemId.java
│ │ │ ├── InvoiceRepository.java
│ │ │ ├── OrderId.java
│ │ │ ├── OrderProcessedEvent.java
│ │ │ └── converter
│ │ │ └── OrderIdAttributeConverter.java
│ │ ├── infra
│ │ └── hibernate
│ │ │ ├── InvoiceIdType.java
│ │ │ ├── InvoiceItemIdType.java
│ │ │ └── package-info.java
│ │ ├── integration
│ │ ├── OrderProcessedEventTranslator.java
│ │ └── OrderStateChangedEvent.java
│ │ └── rest
│ │ ├── client
│ │ ├── Order.java
│ │ ├── OrderClient.java
│ │ ├── OrderItem.java
│ │ └── RecipientAddress.java
│ │ └── controller
│ │ └── InvoiceServiceController.java
│ └── resources
│ └── application.properties
├── orders
├── pom.xml
└── src
│ └── main
│ ├── java
│ └── net
│ │ └── pkhapps
│ │ └── ddd
│ │ └── orders
│ │ ├── DataGenerator.java
│ │ ├── OrdersApp.java
│ │ ├── application
│ │ ├── OrderCatalog.java
│ │ ├── ProductCatalog.java
│ │ └── form
│ │ │ ├── OrderForm.java
│ │ │ ├── OrderItemForm.java
│ │ │ └── RecipientAddressForm.java
│ │ ├── domain
│ │ └── model
│ │ │ ├── Order.java
│ │ │ ├── OrderId.java
│ │ │ ├── OrderItem.java
│ │ │ ├── OrderItemId.java
│ │ │ ├── OrderRepository.java
│ │ │ ├── OrderState.java
│ │ │ ├── OrderStateChange.java
│ │ │ ├── Product.java
│ │ │ ├── ProductId.java
│ │ │ ├── RecipientAddress.java
│ │ │ ├── converter
│ │ │ └── ProductIdConverter.java
│ │ │ └── event
│ │ │ ├── OrderCreated.java
│ │ │ └── OrderStateChanged.java
│ │ ├── infra
│ │ └── hibernate
│ │ │ ├── OrderIdType.java
│ │ │ ├── OrderItemIdType.java
│ │ │ └── package-info.java
│ │ ├── rest
│ │ ├── client
│ │ │ └── ProductCatalogClient.java
│ │ └── controller
│ │ │ └── OrderCatalogController.java
│ │ └── ui
│ │ ├── CreateOrderView.java
│ │ ├── OrderBrowserView.java
│ │ ├── OrderDetailsView.java
│ │ └── TabContainer.java
│ └── resources
│ └── application.properties
├── pom.xml
├── product-catalog
├── pom.xml
└── src
│ └── main
│ ├── java
│ └── net
│ │ └── pkhapps
│ │ └── ddd
│ │ └── productcatalog
│ │ ├── DataGenerator.java
│ │ ├── ProductCatalogApp.java
│ │ ├── application
│ │ └── ProductCatalog.java
│ │ ├── domain
│ │ └── model
│ │ │ ├── Product.java
│ │ │ ├── ProductId.java
│ │ │ └── ProductRepository.java
│ │ ├── infra
│ │ └── hibernate
│ │ │ ├── ProductIdType.java
│ │ │ └── package-info.java
│ │ └── rest
│ │ └── ProductCatalogController.java
│ └── resources
│ └── application.properties
├── shared-kernel
├── pom.xml
└── src
│ ├── main
│ └── java
│ │ └── net
│ │ └── pkhapps
│ │ └── ddd
│ │ └── shared
│ │ ├── SharedConfiguration.java
│ │ ├── domain
│ │ ├── base
│ │ │ ├── AbstractAggregateRoot.java
│ │ │ ├── AbstractEntity.java
│ │ │ ├── ConcurrencySafeDomainObject.java
│ │ │ ├── DeletableDomainObject.java
│ │ │ ├── DomainEvent.java
│ │ │ ├── DomainObject.java
│ │ │ ├── DomainObjectId.java
│ │ │ ├── IdentifiableDomainObject.java
│ │ │ └── ValueObject.java
│ │ ├── financial
│ │ │ ├── Currency.java
│ │ │ ├── CurrencyConverter.java
│ │ │ ├── Money.java
│ │ │ ├── VAT.java
│ │ │ └── converter
│ │ │ │ └── VATAttributeConverter.java
│ │ └── geo
│ │ │ ├── Address.java
│ │ │ ├── CityName.java
│ │ │ ├── Country.java
│ │ │ ├── PostalCode.java
│ │ │ └── converter
│ │ │ ├── CityNameConverter.java
│ │ │ └── PostalCodeConverter.java
│ │ ├── infra
│ │ ├── eventlog
│ │ │ ├── DomainEventLog.java
│ │ │ ├── DomainEventLogAppender.java
│ │ │ ├── DomainEventLogId.java
│ │ │ ├── DomainEventLogService.java
│ │ │ ├── ProcessedRemoteEvent.java
│ │ │ ├── ProcessedRemoteEventRepository.java
│ │ │ ├── RemoteEventLog.java
│ │ │ ├── RemoteEventLogService.java
│ │ │ ├── RemoteEventProcessor.java
│ │ │ ├── RemoteEventTranslator.java
│ │ │ ├── StoredDomainEvent.java
│ │ │ └── StoredDomainEventRepository.java
│ │ ├── hibernate
│ │ │ ├── DomainObjectIdCustomType.java
│ │ │ └── DomainObjectIdTypeDescriptor.java
│ │ └── jackson
│ │ │ └── RawJsonDeserializer.java
│ │ ├── rest
│ │ ├── client
│ │ │ ├── CurrencyConverterClient.java
│ │ │ └── RemoteEventLogServiceClient.java
│ │ └── controller
│ │ │ └── EventLogController.java
│ │ └── ui
│ │ └── converter
│ │ ├── StringToCityNameConverter.java
│ │ └── StringToPostalCodeConverter.java
│ └── test
│ └── java
│ └── net
│ └── pkhapps
│ └── ddd
│ └── shared
│ └── domain
│ └── financial
│ ├── MoneyTest.java
│ └── VATTest.java
└── shipping
├── pom.xml
└── src
└── main
├── java
└── net
│ └── pkhapps
│ └── ddd
│ └── shipping
│ ├── ShippingApp.java
│ ├── application
│ ├── ShippingListCreator.java
│ └── ShippingService.java
│ ├── domain
│ ├── OrderId.java
│ ├── PickingList.java
│ ├── PickingListId.java
│ ├── PickingListItem.java
│ ├── PickingListRepository.java
│ ├── PickingListState.java
│ ├── ProductId.java
│ └── converter
│ │ ├── OrderIdConverter.java
│ │ └── ProductIdConverter.java
│ ├── infra
│ └── hibernate
│ │ ├── PickingListIdType.java
│ │ └── package-info.java
│ ├── integration
│ ├── OrderCreatedEvent.java
│ └── OrderCreatedEventTranslator.java
│ ├── rest
│ ├── client
│ │ ├── Order.java
│ │ ├── OrderCatalogClient.java
│ │ ├── OrderItem.java
│ │ ├── OrderState.java
│ │ └── RecipientAddress.java
│ └── controller
│ │ └── ShippingServiceController.java
│ └── ui
│ ├── PickingListBrowserView.java
│ └── PickingListDetailsView.java
└── resources
└── application.properties
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | .idea/
3 | *.iml
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DDD Example
2 |
3 | This is an example project for my Vaadin TechLunch presentation on November 22, 2018 about
4 | *Domain Driven Design and the Hexagonal Architecture*. It demonstrates how you can use Spring Boot to create a
5 | semi-complex system consisting of multiple bounded contexts, represented here as individual Spring Boot applications.
6 | REST and domain events are used to integrate the contexts with each other.
7 |
8 | Here are some notes to pay special attention to:
9 |
10 | ## Value Objects as Entity IDs
11 |
12 | JPA does not support custom types for `@Id` fields out of the box and you can only use `AttributeConverter` for
13 | non-`@Id` fields. In this example project, I'm using Hibernate custom types which works with one caveat: you can't
14 | use them with `@GeneratedValue` without specifying your own generation strategy.
15 |
16 | The built-in strategies assume an integral data type (such as integer, long, `BigInteger`, etc.) and even if your custom
17 | type is only a wrapper around a long, the built-in strategies will not recognize it as such.
18 |
19 | In this example project, I've solved the problem by using UUIDs and creating the IDs immediately when the entity objects
20 | are created. This has the added advantage of making the ID known and available before anything is persisted, which can
21 | be useful sometimes.
22 |
23 | However, if you want to use sequences or identity columns for ID generation, you have to either create your own ID
24 | generator strategy or use integral types and then wrap them manually in your public API. The advantage with this
25 | approach is that you no longer need any Hibernate custom types but can use `AttributeConverter`s for your reference
26 | fields.
27 |
28 | ## Domain Objects, REST controllers and JSON
29 |
30 | In this application, I'm using `@JsonProperty` annotations directly on my domain objects (entities, value objects, etc.)
31 | and returning them directly from my REST controllers. I'm doing this to save time but in real-world applications this is
32 | not a good practice.
33 |
34 | You will want to have your external REST API to remain as stable as possible, while being able to evolve the domain
35 | model as you learn new things and new requirements emerge. This is hard if your REST API is directly based on the domain
36 | model. Therefore, in a real world application, I very much recommmend you to use dedicated DTOs for your REST APIs.
37 |
38 | ## Lack of Pagination
39 |
40 | To keep things simple, I don't use pagination anywhere (except for the domain event log). In real-world applications,
41 | you should *always* use pagination for unbounded queries (i.e. queries that you don't know for sure will return only
42 | a small and limited number of items).
43 |
44 | ## Domain Event Distribution through REST
45 |
46 | The domain event log and REST protocol is based on the approach presented in *Implementing Domain Driven Design* by
47 | *Vaughn Vernon*. The idea itself is production-ready but the implementation in this example project is not. The handling
48 | of JSON is not optimal, caching headers are missing from the REST responses, there is no test coverage and the solution
49 | is not resilient enough. You can however use what's here as a basis for a production ready implementation, but don't
50 | use it 'as-is' in real-world applications.
51 |
52 | ## @NonNull and @Nullable
53 |
54 | I'm using the `@NonNull` and `@Nullable` annotations everywhere to make it crystal-clear which parameters can or can't
55 | be null and which methods return or don't return null. This is a practice I've started to use lately and so far I really
56 | like it. In this project I'm using the Spring annotations, but I've also used JetBrains' annotations or the
57 | JSR-305 annotations.
58 |
59 | In addition, I often use `Objects.requireNonNull(..)` in the beginning of each method that accepts non-null parameters.
60 |
61 | With the combination of annotations and explicit null-checks I hope to avoid tracking down annoying NPE-bugs in the
62 | future.
63 |
64 | ## The Absence of Getters
65 |
66 | In this project I've avoided getter methods wherever I can. I find it makes the code more fluent and easier to read.
67 | However, I've also discovered that since the usage of getter methods is such a large practice in the Java world, a lof
68 | of conventions and IDE tools no longer work out-of-the-box. This in turn increases the need for annotations and manual
69 | coding.
70 |
71 | Since there is no technical difference beween `person.firstName()` and `person.getFirstName()`, you have to consider
72 | whether leaving out the `get` prefix is really worth the effort.
73 |
74 | ## Setters and Bean Validation
75 |
76 | For all the domain classes, the API is written in such a way that you can't put the aggregate into an inconsistent
77 | state, nor can you edit all the properties after creation (there are virtually no public setters). This means that Bean
78 | Validation (JSR-303) is not needed in the domain layer.
79 |
80 | This in turn means that different objects need to be used in the UI layer for data binding. In this project I'm calling
81 | these objects *form objects*. The naming is from back in the day when you had to fill out forms on paper, mail them
82 | to the retailer who then entered the information into the system. You could also call these objects *request objects*
83 | (since you use them to request a specific operation), *data transfer objects (DTO)s* or something else.
84 |
85 | When an application service receives a form object, it moves the information into the domain layer. To save some time,
86 | you could add JSR-303 annotations to the form object classes and let the bean validator validate the form object before
87 | the application service does anything else with it. Any validation errors could then be reported to the user in a user-
88 | friendly manner.
89 |
--------------------------------------------------------------------------------
/invoicing/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | master-pom
7 | net.pkhapps.ddd
8 | 1.0-SNAPSHOT
9 |
10 | 4.0.0
11 |
12 | invoicing
13 |
14 |
15 |
16 | net.pkhapps.ddd
17 | shared-kernel
18 | ${project.version}
19 |
20 |
21 | org.springframework.boot
22 | spring-boot-starter-web
23 |
24 |
25 | com.vaadin
26 | vaadin-spring-boot-starter
27 |
28 |
29 | com.h2database
30 | h2
31 |
32 |
33 |
34 |
35 |
36 |
37 | org.springframework.boot
38 | spring-boot-maven-plugin
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/InvoicingApp.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing;
2 |
3 | import net.pkhapps.ddd.shared.SharedConfiguration;
4 | import net.pkhapps.ddd.shared.infra.eventlog.RemoteEventLogService;
5 | import net.pkhapps.ddd.shared.rest.client.RemoteEventLogServiceClient;
6 | import org.springframework.beans.factory.annotation.Value;
7 | import org.springframework.boot.SpringApplication;
8 | import org.springframework.boot.autoconfigure.SpringBootApplication;
9 | import org.springframework.boot.autoconfigure.domain.EntityScan;
10 | import org.springframework.context.annotation.Bean;
11 | import org.springframework.context.annotation.Import;
12 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
13 |
14 | import java.time.Clock;
15 |
16 | @SpringBootApplication
17 | @EnableJpaRepositories
18 | @EntityScan
19 | @Import(SharedConfiguration.class)
20 | public class InvoicingApp {
21 |
22 | public static void main(String[] args) {
23 | SpringApplication.run(InvoicingApp.class, args);
24 | }
25 |
26 | @Bean
27 | public Clock clock() {
28 | return Clock.systemUTC();
29 | }
30 |
31 | @Bean
32 | public RemoteEventLogService orderEvents(@Value("${app.orders.url}") String serverUrl,
33 | @Value("${app.orders.connect-timeout-ms}") int connectTimeout,
34 | @Value("${app.orders.read-timeout-ms}") int readTimeout) {
35 | return new RemoteEventLogServiceClient(serverUrl, connectTimeout, readTimeout);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/application/InvoiceCreator.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.application;
2 |
3 | import net.pkhapps.ddd.invoicing.domain.model.Invoice;
4 | import net.pkhapps.ddd.invoicing.domain.model.InvoiceRepository;
5 | import net.pkhapps.ddd.invoicing.domain.model.OrderProcessedEvent;
6 | import net.pkhapps.ddd.invoicing.rest.client.Order;
7 | import net.pkhapps.ddd.invoicing.rest.client.OrderClient;
8 | import net.pkhapps.ddd.shared.domain.geo.Address;
9 | import org.springframework.lang.NonNull;
10 | import org.springframework.stereotype.Service;
11 | import org.springframework.transaction.event.TransactionPhase;
12 | import org.springframework.transaction.event.TransactionalEventListener;
13 |
14 | import java.time.Clock;
15 |
16 | @Service
17 | class InvoiceCreator {
18 |
19 | private static final int DEFAULT_TERMS = 14;
20 | private final InvoiceRepository invoiceRepository;
21 | private final OrderClient orderClient;
22 | private final Clock clock;
23 |
24 | InvoiceCreator(InvoiceRepository invoiceRepository, OrderClient orderClient, Clock clock) {
25 | this.invoiceRepository = invoiceRepository;
26 | this.orderClient = orderClient;
27 | this.clock = clock;
28 | }
29 |
30 | @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
31 | public void onOrderProcessedEvent(OrderProcessedEvent orderProcessedEvent) {
32 | var orderId = orderProcessedEvent.orderId();
33 | // Check if we have done this before (events can receive many times in case of errors)
34 | if (invoiceRepository.findByOrderId(orderId).count() == 0) {
35 | orderClient.findById(orderId).map(this::createInvoice).ifPresent(invoiceRepository::save);
36 | }
37 | }
38 |
39 | @NonNull
40 | private Invoice createInvoice(@NonNull Order order) {
41 | var billingAddress = order.billingAddress();
42 | var invoice = new Invoice(clock.instant(), order.orderId(), DEFAULT_TERMS, billingAddress.name(),
43 | new Address(billingAddress.address1(),
44 | billingAddress.address2(),
45 | billingAddress.cityName(),
46 | billingAddress.postalCode(),
47 | billingAddress.country()),
48 | order.currency());
49 | order.items().forEach(item -> invoice.addItem(item.description(), item.price(), item.vat(), item.qty()));
50 | return invoice;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/application/InvoiceService.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.application;
2 |
3 | import net.pkhapps.ddd.invoicing.domain.model.Invoice;
4 | import net.pkhapps.ddd.invoicing.domain.model.InvoiceId;
5 | import net.pkhapps.ddd.invoicing.domain.model.InvoiceRepository;
6 | import org.springframework.lang.NonNull;
7 | import org.springframework.stereotype.Service;
8 | import org.springframework.transaction.annotation.Propagation;
9 | import org.springframework.transaction.annotation.Transactional;
10 |
11 | import java.util.List;
12 | import java.util.Optional;
13 |
14 | @Service
15 | @Transactional(propagation = Propagation.REQUIRES_NEW)
16 | public class InvoiceService {
17 |
18 | private final InvoiceRepository invoiceRepository;
19 |
20 | InvoiceService(InvoiceRepository invoiceRepository) {
21 | this.invoiceRepository = invoiceRepository;
22 | }
23 |
24 | @NonNull
25 | public List findAll() {
26 | return invoiceRepository.findAll();
27 | }
28 |
29 | @NonNull
30 | public Optional findById(@NonNull InvoiceId invoiceId) {
31 | return invoiceRepository.findById(invoiceId);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/domain/model/Invoice.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.domain.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import net.pkhapps.ddd.shared.domain.base.AbstractAggregateRoot;
5 | import net.pkhapps.ddd.shared.domain.base.ConcurrencySafeDomainObject;
6 | import net.pkhapps.ddd.shared.domain.financial.Currency;
7 | import net.pkhapps.ddd.shared.domain.financial.Money;
8 | import net.pkhapps.ddd.shared.domain.financial.VAT;
9 | import net.pkhapps.ddd.shared.domain.geo.Address;
10 | import org.springframework.lang.NonNull;
11 |
12 | import javax.persistence.*;
13 | import java.time.Instant;
14 | import java.time.LocalDate;
15 | import java.time.ZoneId;
16 | import java.util.HashSet;
17 | import java.util.Objects;
18 | import java.util.Set;
19 | import java.util.stream.Stream;
20 |
21 | @Entity
22 | @Table(name = "invoices")
23 | public class Invoice extends AbstractAggregateRoot implements ConcurrencySafeDomainObject {
24 |
25 | @Version
26 | private Long version;
27 |
28 | @Column(name = "created_on", nullable = false)
29 | private Instant createdOn;
30 |
31 | @Column(name = "order_id", nullable = false)
32 | private OrderId orderId;
33 |
34 | @Column(name = "terms", nullable = false)
35 | private int terms;
36 |
37 | @Column(name = "due_date", nullable = false)
38 | private LocalDate dueDate;
39 |
40 | @Column(name = "recipient_name", nullable = false)
41 | private String recipientName;
42 |
43 | @Embedded
44 | private Address address;
45 |
46 | @OneToMany(mappedBy = "invoice", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
47 | private Set items;
48 |
49 | @Column(name = "currency", nullable = false)
50 | @Enumerated(EnumType.STRING)
51 | private Currency currency;
52 |
53 | @Column(name = "total_excl_vat", nullable = false)
54 | private int totalExcludingVat;
55 |
56 | @Column(name = "total_vat", nullable = false)
57 | private int totalVat;
58 |
59 | @Column(name = "total_incl_vat", nullable = false)
60 | private int totalIncludingVat;
61 |
62 | @SuppressWarnings("unused") // Used by JPA only
63 | private Invoice() {
64 | }
65 |
66 | public Invoice(@NonNull Instant createdOn, @NonNull OrderId orderId, int terms, @NonNull String recipientName,
67 | @NonNull Address address, @NonNull Currency currency) {
68 | super(InvoiceId.randomId(InvoiceId.class));
69 | items = new HashSet<>();
70 | setCreatedOn(createdOn);
71 | setOrderId(orderId);
72 | setTerms(terms);
73 | setRecipientName(recipientName);
74 | setAddress(address);
75 | setCurrency(currency);
76 | calculateTotals();
77 | }
78 |
79 | @NonNull
80 | @JsonProperty("createdOn")
81 | public Instant createdOn() {
82 | return createdOn;
83 | }
84 |
85 | private void setCreatedOn(@NonNull Instant createdOn) {
86 | this.createdOn = Objects.requireNonNull(createdOn, "createdOn must not be null");
87 | }
88 |
89 | @NonNull
90 | @JsonProperty("orderId")
91 | public OrderId orderId() {
92 | return orderId;
93 | }
94 |
95 | private void setOrderId(@NonNull OrderId orderId) {
96 | this.orderId = Objects.requireNonNull(orderId, "orderId must not be null");
97 | }
98 |
99 | @JsonProperty("terms")
100 | public int terms() {
101 | return terms;
102 | }
103 |
104 | private void setTerms(int terms) {
105 | this.terms = terms;
106 | dueDate = LocalDate.ofInstant(createdOn(), ZoneId.systemDefault()).plusDays(terms);
107 | }
108 |
109 | @NonNull
110 | @JsonProperty("dueDate")
111 | public LocalDate dueDate() {
112 | return dueDate;
113 | }
114 |
115 | @NonNull
116 | @JsonProperty("recipientName")
117 | public String recipientName() {
118 | return recipientName;
119 | }
120 |
121 | private void setRecipientName(@NonNull String recipientName) {
122 | this.recipientName = Objects.requireNonNull(recipientName, "recipientName must not be null");
123 | }
124 |
125 | @NonNull
126 | @JsonProperty("address")
127 | public Address address() {
128 | return address;
129 | }
130 |
131 | private void setAddress(@NonNull Address address) {
132 | this.address = Objects.requireNonNull(address, "address must not be null");
133 | }
134 |
135 | @NonNull
136 | @JsonProperty("items")
137 | public Stream items() {
138 | return items.stream();
139 | }
140 |
141 | @NonNull
142 | public InvoiceItem addItem(@NonNull String description, @NonNull Money price, @NonNull VAT vat, int quantity) {
143 | if (price.currency() != currency) {
144 | throw new IllegalArgumentException("Item price must be in same currency as invoice");
145 | }
146 | var item = new InvoiceItem(this, description, price, vat, quantity);
147 | items.add(item);
148 | calculateTotals();
149 | return item;
150 | }
151 |
152 | @NonNull
153 | @JsonProperty("currency")
154 | public Currency currency() {
155 | return currency;
156 | }
157 |
158 | private void setCurrency(@NonNull Currency currency) {
159 | this.currency = Objects.requireNonNull(currency, "currency must not be null");
160 | }
161 |
162 | @NonNull
163 | @JsonProperty("totalExcludingVat")
164 | public Money totalExcludingVat() {
165 | return new Money(currency, totalExcludingVat);
166 | }
167 |
168 | @NonNull
169 | @JsonProperty("totalVat")
170 | public Money getTotalVat() {
171 | return new Money(currency, totalVat);
172 | }
173 |
174 | @NonNull
175 | @JsonProperty("totalIncludingVat")
176 | public Money totalIncludingVat() {
177 | return new Money(currency, totalIncludingVat);
178 | }
179 |
180 | private void calculateTotals() {
181 | totalExcludingVat = items()
182 | .map(InvoiceItem::subtotalExcludingVat)
183 | .reduce(new Money(currency, 0), Money::add)
184 | .fixedPointAmount();
185 | totalVat = items()
186 | .map(InvoiceItem::subtotalVat)
187 | .reduce(new Money(currency, 0), Money::add)
188 | .fixedPointAmount();
189 | totalIncludingVat = totalExcludingVat + totalVat;
190 | }
191 |
192 | @Override
193 | public Long version() {
194 | return version;
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/domain/model/InvoiceId.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.domain.model;
2 |
3 | import net.pkhapps.ddd.shared.domain.base.DomainObjectId;
4 | import org.springframework.lang.NonNull;
5 |
6 | public class InvoiceId extends DomainObjectId {
7 | public InvoiceId(@NonNull String uuid) {
8 | super(uuid);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/domain/model/InvoiceItem.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.domain.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import net.pkhapps.ddd.shared.domain.base.AbstractEntity;
5 | import net.pkhapps.ddd.shared.domain.base.DomainObjectId;
6 | import net.pkhapps.ddd.shared.domain.financial.Currency;
7 | import net.pkhapps.ddd.shared.domain.financial.Money;
8 | import net.pkhapps.ddd.shared.domain.financial.VAT;
9 | import org.springframework.lang.NonNull;
10 |
11 | import javax.persistence.*;
12 | import java.util.Objects;
13 |
14 | @Entity
15 | @Table(name = "invoice_items")
16 | public class InvoiceItem extends AbstractEntity {
17 |
18 | @ManyToOne(optional = false)
19 | @JoinColumn(name = "invoice_id", nullable = false)
20 | private Invoice invoice;
21 |
22 | @Column(name = "description", nullable = false)
23 | private String description;
24 |
25 | @Column(name = "currency", nullable = false)
26 | @Enumerated(EnumType.STRING)
27 | private Currency currency;
28 |
29 | @Column(name = "price", nullable = false)
30 | private int price;
31 |
32 | @Column(name = "vat", nullable = false)
33 | private VAT vat;
34 |
35 | @Column(name = "qty", nullable = false)
36 | private int quantity;
37 |
38 | @Column(name = "subtotal_excl_vat", nullable = false)
39 | private int subtotalExcludingVat;
40 |
41 | @Column(name = "subtotal_vat", nullable = false)
42 | private int subtotalVat;
43 |
44 | @Column(name = "subtotal_incl_vat", nullable = false)
45 | private int subtotalIncludingVat;
46 |
47 | @SuppressWarnings("unused") // Used by JPA only
48 | private InvoiceItem() {
49 | }
50 |
51 | InvoiceItem(@NonNull Invoice invoice, @NonNull String description, @NonNull Money price, @NonNull VAT vat, int quantity) {
52 | super(DomainObjectId.randomId(InvoiceItemId.class));
53 | setInvoice(invoice);
54 | setDescription(description);
55 | setPrice(price);
56 | setVat(vat);
57 | setQuantity(quantity);
58 | calculateSubTotals();
59 | }
60 |
61 | @NonNull
62 | public Invoice invoice() {
63 | return invoice;
64 | }
65 |
66 | private void setInvoice(@NonNull Invoice invoice) {
67 | this.invoice = Objects.requireNonNull(invoice, "invoice must not be null");
68 | }
69 |
70 | @NonNull
71 | @JsonProperty("description")
72 | public String description() {
73 | return description;
74 | }
75 |
76 | private void setDescription(@NonNull String description) {
77 | this.description = Objects.requireNonNull(description, "description must not be null");
78 | }
79 |
80 | @NonNull
81 | @JsonProperty("price")
82 | public Money price() {
83 | return Money.valueOf(currency, price);
84 | }
85 |
86 | private void setPrice(@NonNull Money price) {
87 | Objects.requireNonNull(price, "price must not be null");
88 | this.currency = price.currency();
89 | this.price = price.fixedPointAmount();
90 | }
91 |
92 | @NonNull
93 | @JsonProperty("vat")
94 | public VAT vat() {
95 | return vat;
96 | }
97 |
98 | private void setVat(@NonNull VAT vat) {
99 | this.vat = Objects.requireNonNull(vat, "vat must not be null");
100 | }
101 |
102 | @JsonProperty("qty")
103 | public int quantity() {
104 | return quantity;
105 | }
106 |
107 | private void setQuantity(int quantity) {
108 | this.quantity = quantity;
109 | }
110 |
111 | @NonNull
112 | @JsonProperty("subtotalExcludingVat")
113 | public Money subtotalExcludingVat() {
114 | return new Money(currency, subtotalExcludingVat);
115 | }
116 |
117 | @NonNull
118 | @JsonProperty("subtotalIncludingVat")
119 | public Money subtotalIncludingVat() {
120 | return new Money(currency, subtotalIncludingVat);
121 | }
122 |
123 | @NonNull
124 | @JsonProperty("subtotalVat")
125 | public Money subtotalVat() {
126 | return new Money(currency, subtotalVat);
127 | }
128 |
129 | private void calculateSubTotals() {
130 | subtotalExcludingVat = price().multiply(quantity).fixedPointAmount();
131 | subtotalVat = vat.calculateTax(new Money(currency, subtotalExcludingVat)).fixedPointAmount();
132 | subtotalIncludingVat = subtotalExcludingVat + subtotalVat;
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/domain/model/InvoiceItemId.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.domain.model;
2 |
3 | import net.pkhapps.ddd.shared.domain.base.DomainObjectId;
4 | import org.springframework.lang.NonNull;
5 |
6 | public class InvoiceItemId extends DomainObjectId {
7 | public InvoiceItemId(@NonNull String uuid) {
8 | super(uuid);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/domain/model/InvoiceRepository.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.domain.model;
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository;
4 | import org.springframework.lang.NonNull;
5 |
6 | import java.util.stream.Stream;
7 |
8 | public interface InvoiceRepository extends JpaRepository {
9 | @NonNull
10 | Stream findByOrderId(@NonNull OrderId orderId);
11 | }
12 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/domain/model/OrderId.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.domain.model;
2 |
3 | import net.pkhapps.ddd.shared.domain.base.DomainObjectId;
4 | import org.springframework.lang.NonNull;
5 |
6 | public class OrderId extends DomainObjectId {
7 | public OrderId(@NonNull String uuid) {
8 | super(uuid);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/domain/model/OrderProcessedEvent.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.domain.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonCreator;
4 | import com.fasterxml.jackson.annotation.JsonProperty;
5 | import net.pkhapps.ddd.shared.domain.base.DomainEvent;
6 | import org.springframework.lang.NonNull;
7 |
8 | import java.time.Instant;
9 | import java.util.Objects;
10 |
11 | public class OrderProcessedEvent implements DomainEvent {
12 |
13 | @JsonProperty("orderId")
14 | private final OrderId orderId;
15 | @JsonProperty("occurredOn")
16 | private final Instant occurredOn;
17 |
18 | @JsonCreator
19 | public OrderProcessedEvent(@JsonProperty("orderId") @NonNull OrderId orderId,
20 | @JsonProperty("occurredOn") @NonNull Instant occurredOn) {
21 | this.orderId = Objects.requireNonNull(orderId, "orderId must not be null");
22 | this.occurredOn = Objects.requireNonNull(occurredOn, "occurredOn must not be null");
23 | }
24 |
25 | @NonNull
26 | public OrderId orderId() {
27 | return orderId;
28 | }
29 |
30 | @Override
31 | @NonNull
32 | public Instant occurredOn() {
33 | return occurredOn;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/domain/model/converter/OrderIdAttributeConverter.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.domain.model.converter;
2 |
3 | import net.pkhapps.ddd.invoicing.domain.model.OrderId;
4 |
5 | import javax.persistence.AttributeConverter;
6 | import javax.persistence.Converter;
7 |
8 | @Converter(autoApply = true)
9 | public class OrderIdAttributeConverter implements AttributeConverter {
10 |
11 | @Override
12 | public String convertToDatabaseColumn(OrderId attribute) {
13 | return attribute == null ? null : attribute.toUUID();
14 | }
15 |
16 | @Override
17 | public OrderId convertToEntityAttribute(String dbData) {
18 | return dbData == null ? null : new OrderId(dbData);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/infra/hibernate/InvoiceIdType.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.infra.hibernate;
2 |
3 | import net.pkhapps.ddd.invoicing.domain.model.InvoiceId;
4 | import net.pkhapps.ddd.shared.infra.hibernate.DomainObjectIdCustomType;
5 | import net.pkhapps.ddd.shared.infra.hibernate.DomainObjectIdTypeDescriptor;
6 |
7 | public class InvoiceIdType extends DomainObjectIdCustomType {
8 |
9 | private static final DomainObjectIdTypeDescriptor TYPE_DESCRIPTOR =
10 | new DomainObjectIdTypeDescriptor<>(InvoiceId.class, InvoiceId::new);
11 |
12 | public InvoiceIdType() {
13 | super(TYPE_DESCRIPTOR);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/infra/hibernate/InvoiceItemIdType.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.infra.hibernate;
2 |
3 | import net.pkhapps.ddd.invoicing.domain.model.InvoiceItemId;
4 | import net.pkhapps.ddd.shared.infra.hibernate.DomainObjectIdCustomType;
5 | import net.pkhapps.ddd.shared.infra.hibernate.DomainObjectIdTypeDescriptor;
6 |
7 | public class InvoiceItemIdType extends DomainObjectIdCustomType {
8 |
9 | private static final DomainObjectIdTypeDescriptor TYPE_DESCRIPTOR =
10 | new DomainObjectIdTypeDescriptor<>(InvoiceItemId.class, InvoiceItemId::new);
11 |
12 | public InvoiceItemIdType() {
13 | super(TYPE_DESCRIPTOR);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/infra/hibernate/package-info.java:
--------------------------------------------------------------------------------
1 | @TypeDefs({
2 | @TypeDef(defaultForType = InvoiceId.class, typeClass = InvoiceIdType.class),
3 | @TypeDef(defaultForType = InvoiceItemId.class, typeClass = InvoiceItemIdType.class)
4 | })
5 | package net.pkhapps.ddd.invoicing.infra.hibernate;
6 |
7 | import net.pkhapps.ddd.invoicing.domain.model.InvoiceId;
8 | import net.pkhapps.ddd.invoicing.domain.model.InvoiceItemId;
9 | import org.hibernate.annotations.TypeDef;
10 | import org.hibernate.annotations.TypeDefs;
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/integration/OrderProcessedEventTranslator.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.integration;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import net.pkhapps.ddd.invoicing.domain.model.OrderProcessedEvent;
5 | import net.pkhapps.ddd.shared.domain.base.DomainEvent;
6 | import net.pkhapps.ddd.shared.infra.eventlog.RemoteEventTranslator;
7 | import net.pkhapps.ddd.shared.infra.eventlog.StoredDomainEvent;
8 | import org.springframework.lang.NonNull;
9 | import org.springframework.stereotype.Service;
10 |
11 | import java.util.Optional;
12 |
13 | @Service
14 | class OrderProcessedEventTranslator implements RemoteEventTranslator {
15 |
16 | private final ObjectMapper objectMapper;
17 |
18 | OrderProcessedEventTranslator(ObjectMapper objectMapper) {
19 | this.objectMapper = objectMapper;
20 | }
21 |
22 | @Override
23 | public boolean supports(@NonNull StoredDomainEvent remoteEvent) {
24 | return remoteEvent.domainEventClassName().equals("net.pkhapps.ddd.orders.domain.model.event.OrderStateChanged");
25 | }
26 |
27 | @Override
28 | @NonNull
29 | public Optional translate(@NonNull StoredDomainEvent remoteEvent) {
30 | var orderStateChanged = remoteEvent.toDomainEvent(objectMapper, OrderStateChangedEvent.class);
31 | if (orderStateChanged.state().equals("PROCESSED")) {
32 | return Optional.of(new OrderProcessedEvent(orderStateChanged.orderId(), orderStateChanged.occurredOn()));
33 | }
34 | return Optional.empty();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/integration/OrderStateChangedEvent.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.integration;
2 |
3 | import com.fasterxml.jackson.annotation.JsonCreator;
4 | import com.fasterxml.jackson.annotation.JsonProperty;
5 | import net.pkhapps.ddd.invoicing.domain.model.OrderId;
6 | import net.pkhapps.ddd.shared.domain.base.DomainEvent;
7 | import org.springframework.lang.NonNull;
8 |
9 | import java.time.Instant;
10 | import java.util.Objects;
11 |
12 | class OrderStateChangedEvent implements DomainEvent {
13 |
14 | private final Instant occurredOn;
15 | private final OrderId orderId;
16 | private final String state;
17 |
18 | @JsonCreator
19 | public OrderStateChangedEvent(@NonNull @JsonProperty("occurredOn") Instant occurredOn,
20 | @NonNull @JsonProperty("orderId") OrderId orderId,
21 | @NonNull @JsonProperty("state") String state) {
22 | this.occurredOn = Objects.requireNonNull(occurredOn, "occurredOn must not be null");
23 | this.orderId = Objects.requireNonNull(orderId, "orderId must not be null");
24 | this.state = Objects.requireNonNull(state, "state must not be null");
25 | }
26 |
27 | @Override
28 | @NonNull
29 | public Instant occurredOn() {
30 | return occurredOn;
31 | }
32 |
33 | @NonNull
34 | public OrderId orderId() {
35 | return orderId;
36 | }
37 |
38 | @NonNull
39 | public String state() {
40 | return state;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/rest/client/Order.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.rest.client;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import net.pkhapps.ddd.invoicing.domain.model.OrderId;
5 | import net.pkhapps.ddd.shared.domain.financial.Currency;
6 |
7 | import java.util.Set;
8 | import java.util.stream.Stream;
9 |
10 | public class Order {
11 |
12 | @JsonProperty("id")
13 | private OrderId id;
14 |
15 | @JsonProperty("currency")
16 | private Currency currency;
17 |
18 | @JsonProperty("billingAddress")
19 | private RecipientAddress billingAddress;
20 |
21 | @JsonProperty("items")
22 | private Set items;
23 |
24 | Order() {
25 | }
26 |
27 | public OrderId orderId() {
28 | return id;
29 | }
30 |
31 | public Currency currency() {
32 | return currency;
33 | }
34 |
35 | public RecipientAddress billingAddress() {
36 | return billingAddress;
37 | }
38 |
39 | public Stream items() {
40 | return items.stream();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/rest/client/OrderClient.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.rest.client;
2 |
3 | import net.pkhapps.ddd.invoicing.domain.model.OrderId;
4 | import org.springframework.beans.factory.annotation.Value;
5 | import org.springframework.http.ResponseEntity;
6 | import org.springframework.http.client.SimpleClientHttpRequestFactory;
7 | import org.springframework.lang.NonNull;
8 | import org.springframework.stereotype.Service;
9 | import org.springframework.web.client.RestTemplate;
10 | import org.springframework.web.util.UriComponentsBuilder;
11 |
12 | import java.util.Optional;
13 |
14 | @Service
15 | public class OrderClient {
16 |
17 | private final RestTemplate restTemplate;
18 | private final String serverUrl;
19 |
20 | OrderClient(@Value("${app.orders.url}") String serverUrl,
21 | @Value("${app.orders.connect-timeout-ms}") int connectTimeout,
22 | @Value("${app.orders.read-timeout-ms}") int readTimeout) {
23 | this.serverUrl = serverUrl;
24 | restTemplate = new RestTemplate();
25 | var requestFactory = new SimpleClientHttpRequestFactory();
26 | requestFactory.setConnectTimeout(connectTimeout);
27 | requestFactory.setReadTimeout(readTimeout);
28 | restTemplate.setRequestFactory(requestFactory);
29 | }
30 |
31 | @NonNull
32 | public Optional findById(@NonNull OrderId orderId) {
33 | var uri = UriComponentsBuilder.fromUriString(serverUrl).path("/api/orders/{id}");
34 | try {
35 | ResponseEntity response = restTemplate.getForEntity(uri.build(orderId.toUUID()), Order.class);
36 | return Optional.ofNullable(response.getBody());
37 | } catch (Exception ex) {
38 | return Optional.empty();
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/rest/client/OrderItem.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.rest.client;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import net.pkhapps.ddd.shared.domain.financial.Money;
5 | import net.pkhapps.ddd.shared.domain.financial.VAT;
6 |
7 | public class OrderItem {
8 |
9 | @JsonProperty("description")
10 | private String description;
11 |
12 | @JsonProperty("price")
13 | private Money price;
14 |
15 | @JsonProperty("valueAddedTax")
16 | private VAT vat;
17 |
18 | @JsonProperty("qty")
19 | private int qty;
20 |
21 | OrderItem() {
22 | }
23 |
24 | public String description() {
25 | return description;
26 | }
27 |
28 | public Money price() {
29 | return price;
30 | }
31 |
32 | public VAT vat() {
33 | return vat;
34 | }
35 |
36 | public int qty() {
37 | return qty;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/rest/client/RecipientAddress.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.rest.client;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import net.pkhapps.ddd.shared.domain.geo.CityName;
5 | import net.pkhapps.ddd.shared.domain.geo.Country;
6 | import net.pkhapps.ddd.shared.domain.geo.PostalCode;
7 |
8 | public class RecipientAddress {
9 |
10 | @JsonProperty("name")
11 | private String name;
12 |
13 | @JsonProperty("address1")
14 | private String address1;
15 |
16 | @JsonProperty("address2")
17 | private String address2;
18 |
19 | @JsonProperty("postalCode")
20 | private PostalCode postalCode;
21 |
22 | @JsonProperty("city")
23 | private CityName cityName;
24 |
25 | @JsonProperty("country")
26 | private Country country;
27 |
28 | RecipientAddress() {
29 | }
30 |
31 | public String name() {
32 | return name;
33 | }
34 |
35 | public String address1() {
36 | return address1;
37 | }
38 |
39 | public String address2() {
40 | return address2;
41 | }
42 |
43 | public PostalCode postalCode() {
44 | return postalCode;
45 | }
46 |
47 | public CityName cityName() {
48 | return cityName;
49 | }
50 |
51 | public Country country() {
52 | return country;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/invoicing/src/main/java/net/pkhapps/ddd/invoicing/rest/controller/InvoiceServiceController.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.invoicing.rest.controller;
2 |
3 | import net.pkhapps.ddd.invoicing.application.InvoiceService;
4 | import net.pkhapps.ddd.invoicing.domain.model.Invoice;
5 | import net.pkhapps.ddd.invoicing.domain.model.InvoiceId;
6 | import org.springframework.http.ResponseEntity;
7 | import org.springframework.web.bind.annotation.GetMapping;
8 | import org.springframework.web.bind.annotation.PathVariable;
9 | import org.springframework.web.bind.annotation.RequestMapping;
10 | import org.springframework.web.bind.annotation.RestController;
11 |
12 | import java.util.List;
13 |
14 | @RestController
15 | @RequestMapping("/api/invoices")
16 | class InvoiceServiceController {
17 |
18 | private final InvoiceService invoiceService;
19 |
20 | InvoiceServiceController(InvoiceService invoiceService) {
21 | this.invoiceService = invoiceService;
22 | }
23 |
24 | @GetMapping
25 | public List findAll() {
26 | return invoiceService.findAll();
27 | }
28 |
29 | @GetMapping("/{id}")
30 | public ResponseEntity findById(@PathVariable("id") String invoiceId) {
31 | return invoiceService.findById(new InvoiceId(invoiceId))
32 | .map(ResponseEntity::ok)
33 | .orElse(ResponseEntity.notFound().build());
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/invoicing/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.h2.console.enabled=true
2 | spring.h2.console.path=/h2-console
3 | spring.datasource.url=jdbc:h2:mem:invoicing;DB_CLOSE_DELAY=-1.
4 | server.port=9004
5 |
6 | app.orders.url=http://localhost:9002
7 | app.orders.connect-timeout-ms=1000
8 | app.orders.read-timeout-ms=5000
9 | logging.level.net.pkhapps.ddd=debug
10 |
--------------------------------------------------------------------------------
/orders/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | master-pom
7 | net.pkhapps.ddd
8 | 1.0-SNAPSHOT
9 |
10 | 4.0.0
11 |
12 | orders
13 |
14 |
15 |
16 | net.pkhapps.ddd
17 | shared-kernel
18 | ${project.version}
19 |
20 |
21 | org.springframework.boot
22 | spring-boot-starter-web
23 |
24 |
25 | com.vaadin
26 | vaadin-spring-boot-starter
27 |
28 |
29 | com.h2database
30 | h2
31 |
32 |
33 |
34 |
35 |
36 |
37 | org.springframework.boot
38 | spring-boot-maven-plugin
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/DataGenerator.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders;
2 |
3 | import net.pkhapps.ddd.orders.domain.model.*;
4 | import net.pkhapps.ddd.shared.domain.financial.Currency;
5 | import net.pkhapps.ddd.shared.domain.financial.CurrencyConverter;
6 | import net.pkhapps.ddd.shared.domain.financial.Money;
7 | import net.pkhapps.ddd.shared.domain.financial.VAT;
8 | import net.pkhapps.ddd.shared.domain.geo.CityName;
9 | import net.pkhapps.ddd.shared.domain.geo.Country;
10 | import net.pkhapps.ddd.shared.domain.geo.PostalCode;
11 |
12 | import javax.annotation.PostConstruct;
13 | import java.time.Instant;
14 | import java.util.UUID;
15 |
16 | //@Component Disabled for now
17 | public class DataGenerator {
18 |
19 | private final OrderRepository orderRepository;
20 | private final CurrencyConverter currencyConverter;
21 |
22 | public DataGenerator(OrderRepository orderRepository, CurrencyConverter currencyConverter) {
23 | this.orderRepository = orderRepository;
24 | this.currencyConverter = currencyConverter;
25 | }
26 |
27 | @PostConstruct
28 | public void generateData() {
29 | System.out.println(orderRepository.findAll());
30 |
31 | Order order = new Order(Instant.now(), Currency.EUR,
32 | new RecipientAddress("Joe Cool", "Street", null, new CityName("City"), new PostalCode("12345"), Country.FINLAND),
33 | new RecipientAddress("Maxwell Smart", "Road", null, new CityName("Town"), new PostalCode("67890"), Country.SWEDEN));
34 | order.addItem(new Product(new ProductId(UUID.randomUUID().toString()), "Product 1", new VAT(24), new Money(Currency.EUR, 25.00)), 10, currencyConverter);
35 | order.addItem(new Product(new ProductId(UUID.randomUUID().toString()), "Product 2", new VAT(24), new Money(Currency.EUR, 150.00)), 1, currencyConverter);
36 | orderRepository.save(order);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/OrdersApp.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders;
2 |
3 | import net.pkhapps.ddd.shared.SharedConfiguration;
4 | import org.springframework.boot.SpringApplication;
5 | import org.springframework.boot.autoconfigure.SpringBootApplication;
6 | import org.springframework.boot.autoconfigure.domain.EntityScan;
7 | import org.springframework.context.annotation.Bean;
8 | import org.springframework.context.annotation.Import;
9 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
10 |
11 | import java.time.Clock;
12 |
13 | @SpringBootApplication
14 | @EnableJpaRepositories
15 | @EntityScan
16 | @Import(SharedConfiguration.class)
17 | public class OrdersApp {
18 |
19 | public static void main(String[] args) {
20 | SpringApplication.run(OrdersApp.class, args);
21 | }
22 |
23 | @Bean
24 | public Clock clock() {
25 | return Clock.systemUTC();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/application/OrderCatalog.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.application;
2 |
3 | import net.pkhapps.ddd.orders.application.form.OrderForm;
4 | import net.pkhapps.ddd.orders.application.form.RecipientAddressForm;
5 | import net.pkhapps.ddd.orders.domain.model.Order;
6 | import net.pkhapps.ddd.orders.domain.model.OrderId;
7 | import net.pkhapps.ddd.orders.domain.model.OrderRepository;
8 | import net.pkhapps.ddd.orders.domain.model.RecipientAddress;
9 | import net.pkhapps.ddd.orders.domain.model.event.OrderCreated;
10 | import net.pkhapps.ddd.shared.domain.financial.CurrencyConverter;
11 | import org.springframework.context.ApplicationEventPublisher;
12 | import org.springframework.lang.NonNull;
13 | import org.springframework.stereotype.Service;
14 | import org.springframework.transaction.annotation.Propagation;
15 | import org.springframework.transaction.annotation.Transactional;
16 |
17 | import javax.annotation.Nonnull;
18 | import javax.validation.ConstraintViolationException;
19 | import javax.validation.Validator;
20 | import java.time.Clock;
21 | import java.util.List;
22 | import java.util.Objects;
23 | import java.util.Optional;
24 |
25 | @Service
26 | @Transactional(propagation = Propagation.REQUIRES_NEW)
27 | public class OrderCatalog {
28 |
29 | private final Validator validator;
30 | private final OrderRepository orderRepository;
31 | private final ApplicationEventPublisher applicationEventPublisher;
32 | private final Clock clock;
33 | private final CurrencyConverter currencyConverter;
34 |
35 | OrderCatalog(Validator validator,
36 | OrderRepository orderRepository,
37 | ApplicationEventPublisher applicationEventPublisher,
38 | Clock clock,
39 | CurrencyConverter currencyConverter) {
40 | this.validator = validator;
41 | this.orderRepository = orderRepository;
42 | this.applicationEventPublisher = applicationEventPublisher;
43 | this.clock = clock;
44 | this.currencyConverter = currencyConverter;
45 | }
46 |
47 | @NonNull
48 | public OrderId createOrder(@NonNull OrderForm form) {
49 | Objects.requireNonNull(form, "form must not be null");
50 | var constraintViolations = validator.validate(form);
51 | if (constraintViolations.size() > 0) {
52 | throw new ConstraintViolationException("The OrderForm is not valid", constraintViolations);
53 | }
54 | var order = orderRepository.saveAndFlush(toDomainModel(form));
55 | applicationEventPublisher.publishEvent(new OrderCreated(order.id(), order.orderedOn()));
56 | return order.id();
57 | }
58 |
59 | @NonNull
60 | public List findAll() {
61 | return orderRepository.findAll();
62 | }
63 |
64 | @NonNull
65 | public Optional findById(@NonNull OrderId orderId) {
66 | Objects.requireNonNull(orderId, "orderId must not be null");
67 | return orderRepository.findById(orderId);
68 | }
69 |
70 | public void startProcessing(@Nonnull OrderId orderId) {
71 | orderRepository.findById(orderId).ifPresent(order -> {
72 | order.startProcessing(clock);
73 | orderRepository.save(order);
74 | });
75 | }
76 |
77 | public void finishProcessing(@Nonnull OrderId orderId) {
78 | orderRepository.findById(orderId).ifPresent(order -> {
79 | order.finishProcessing(clock);
80 | orderRepository.save(order);
81 | });
82 | }
83 |
84 | @NonNull
85 | private Order toDomainModel(@NonNull OrderForm orderForm) {
86 | var order = new Order(clock.instant(), orderForm.getCurrency(),
87 | toDomainModel(orderForm.getBillingAddress()),
88 | toDomainModel(orderForm.getShippingAddress()));
89 | orderForm.getItems().forEach(item -> order.addItem(item.getProduct(), item.getQuantity(), currencyConverter));
90 | return order;
91 | }
92 |
93 | @NonNull
94 | private RecipientAddress toDomainModel(@NonNull RecipientAddressForm form) {
95 | return new RecipientAddress(form.getName(), form.getAddressLine1(), form.getAddressLine2(), form.getCity(),
96 | form.getPostalCode(), form.getCountry());
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/application/ProductCatalog.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.application;
2 |
3 | import net.pkhapps.ddd.orders.domain.model.Product;
4 |
5 | import java.util.List;
6 |
7 | public interface ProductCatalog {
8 |
9 | List findAll();
10 | }
11 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/application/form/OrderForm.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.application.form;
2 |
3 | import net.pkhapps.ddd.shared.domain.financial.Currency;
4 |
5 | import javax.validation.Valid;
6 | import javax.validation.constraints.NotEmpty;
7 | import javax.validation.constraints.NotNull;
8 | import java.io.Serializable;
9 | import java.util.ArrayList;
10 | import java.util.List;
11 |
12 | public class OrderForm implements Serializable {
13 |
14 | @NotNull
15 | private Currency currency;
16 | @Valid
17 | @NotNull
18 | private RecipientAddressForm billingAddress = new RecipientAddressForm();
19 | @Valid
20 | @NotNull
21 | private RecipientAddressForm shippingAddress = new RecipientAddressForm();
22 | @Valid
23 | @NotEmpty
24 | private List items = new ArrayList<>();
25 |
26 | public Currency getCurrency() {
27 | return currency;
28 | }
29 |
30 | public void setCurrency(Currency currency) {
31 | this.currency = currency;
32 | }
33 |
34 | public RecipientAddressForm getBillingAddress() {
35 | return billingAddress;
36 | }
37 |
38 | public RecipientAddressForm getShippingAddress() {
39 | return shippingAddress;
40 | }
41 |
42 | public List getItems() {
43 | return items;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/application/form/OrderItemForm.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.application.form;
2 |
3 | import net.pkhapps.ddd.orders.domain.model.Product;
4 |
5 | import javax.validation.constraints.Min;
6 | import javax.validation.constraints.NotNull;
7 | import java.io.Serializable;
8 |
9 | public class OrderItemForm implements Serializable {
10 |
11 | @NotNull
12 | private Product product;
13 | @Min(1)
14 | private int quantity = 1;
15 |
16 | public Product getProduct() {
17 | return product;
18 | }
19 |
20 | public void setProduct(Product product) {
21 | this.product = product;
22 | }
23 |
24 | public int getQuantity() {
25 | return quantity;
26 | }
27 |
28 | public void setQuantity(int quantity) {
29 | this.quantity = quantity;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/application/form/RecipientAddressForm.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.application.form;
2 |
3 | import net.pkhapps.ddd.shared.domain.geo.CityName;
4 | import net.pkhapps.ddd.shared.domain.geo.Country;
5 | import net.pkhapps.ddd.shared.domain.geo.PostalCode;
6 |
7 | import javax.validation.constraints.NotEmpty;
8 | import javax.validation.constraints.NotNull;
9 | import java.io.Serializable;
10 |
11 | public class RecipientAddressForm implements Serializable {
12 |
13 | @NotEmpty
14 | private String name;
15 | @NotEmpty
16 | private String addressLine1;
17 | private String addressLine2;
18 | @NotNull
19 | private CityName city;
20 | @NotNull
21 | private PostalCode postalCode;
22 | @NotNull
23 | private Country country;
24 |
25 | public String getName() {
26 | return name;
27 | }
28 |
29 | public void setName(String name) {
30 | this.name = name;
31 | }
32 |
33 | public String getAddressLine1() {
34 | return addressLine1;
35 | }
36 |
37 | public void setAddressLine1(String addressLine1) {
38 | this.addressLine1 = addressLine1;
39 | }
40 |
41 | public String getAddressLine2() {
42 | return addressLine2;
43 | }
44 |
45 | public void setAddressLine2(String addressLine2) {
46 | this.addressLine2 = addressLine2;
47 | }
48 |
49 | public CityName getCity() {
50 | return city;
51 | }
52 |
53 | public void setCity(CityName city) {
54 | this.city = city;
55 | }
56 |
57 | public PostalCode getPostalCode() {
58 | return postalCode;
59 | }
60 |
61 | public void setPostalCode(PostalCode postalCode) {
62 | this.postalCode = postalCode;
63 | }
64 |
65 | public Country getCountry() {
66 | return country;
67 | }
68 |
69 | public void setCountry(Country country) {
70 | this.country = country;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/domain/model/OrderId.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.domain.model;
2 |
3 | import net.pkhapps.ddd.shared.domain.base.DomainObjectId;
4 |
5 | public class OrderId extends DomainObjectId {
6 | public OrderId(String uuid) {
7 | super(uuid);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/domain/model/OrderItem.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.domain.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import net.pkhapps.ddd.shared.domain.base.AbstractEntity;
5 | import net.pkhapps.ddd.shared.domain.base.DomainObjectId;
6 | import net.pkhapps.ddd.shared.domain.financial.Currency;
7 | import net.pkhapps.ddd.shared.domain.financial.Money;
8 | import net.pkhapps.ddd.shared.domain.financial.VAT;
9 | import org.springframework.lang.NonNull;
10 |
11 | import javax.persistence.*;
12 | import java.util.Objects;
13 |
14 | @Entity
15 | @Table(name = "order_items", uniqueConstraints = @UniqueConstraint(columnNames = {"product_id", "order_id"}))
16 | public class OrderItem extends AbstractEntity {
17 |
18 | @Column(name = "product_id", nullable = false)
19 | private ProductId productId;
20 | @Column(name = "item_description", nullable = false)
21 | private String itemDescription;
22 | @Column(name = "item_currency", nullable = false)
23 | @Enumerated(EnumType.STRING)
24 | private Currency itemPriceCurrency;
25 | @Column(name = "item_price", nullable = false)
26 | private int itemPrice;
27 | @Column(name = "value_added_tax", nullable = false)
28 | private VAT valueAddedTax;
29 | @Column(name = "qty", nullable = false)
30 | private int quantity;
31 |
32 | @SuppressWarnings("unused") // Used by JPA only
33 | private OrderItem() {
34 | }
35 |
36 | OrderItem(@NonNull ProductId productId, @NonNull String itemDescription, @NonNull Money itemPrice,
37 | @NonNull VAT valueAddedTax) {
38 | super(DomainObjectId.randomId(OrderItemId.class));
39 | setProductId(productId);
40 | setItemDescription(itemDescription);
41 | setItemPrice(itemPrice);
42 | setValueAddedTax(valueAddedTax);
43 | }
44 |
45 | @NonNull
46 | @JsonProperty("productId")
47 | public ProductId productId() {
48 | return productId;
49 | }
50 |
51 | private void setProductId(@NonNull ProductId productId) {
52 | this.productId = Objects.requireNonNull(productId, "productId must not be null");
53 | }
54 |
55 | @NonNull
56 | @JsonProperty("description")
57 | public String itemDescription() {
58 | return itemDescription;
59 | }
60 |
61 | private void setItemDescription(@NonNull String itemDescription) {
62 | this.itemDescription = Objects.requireNonNull(itemDescription, "itemDescription must not be null");
63 | }
64 |
65 | @NonNull
66 | @JsonProperty("price")
67 | public Money itemPrice() {
68 | return Money.valueOf(itemPriceCurrency, itemPrice);
69 | }
70 |
71 | private void setItemPrice(@NonNull Money itemPrice) {
72 | Objects.requireNonNull(itemPrice, "itemPrice must not be null");
73 | this.itemPrice = itemPrice.fixedPointAmount();
74 | this.itemPriceCurrency = itemPrice.currency();
75 | }
76 |
77 | @NonNull
78 | @JsonProperty("valueAddedTax")
79 | public VAT valueAddedTax() {
80 | return valueAddedTax;
81 | }
82 |
83 | private void setValueAddedTax(@NonNull VAT valueAddedTax) {
84 | this.valueAddedTax = Objects.requireNonNull(valueAddedTax, "valueAddedTax must not be null");
85 | }
86 |
87 | @NonNull
88 | @JsonProperty("qty")
89 | public int quantity() {
90 | return quantity;
91 | }
92 |
93 | public void setQuantity(int quantity) {
94 | if (quantity < 0) {
95 | throw new IllegalArgumentException("Quantity cannot be negative");
96 | }
97 | this.quantity = quantity;
98 | }
99 |
100 | @NonNull
101 | @JsonProperty("subtotalExcludingVAT")
102 | public Money subtotalExcludingTax() {
103 | return itemPrice().multiply(quantity());
104 | }
105 |
106 | @NonNull
107 | @JsonProperty("subtotalVAT")
108 | public Money subtotalTax() {
109 | return valueAddedTax().calculateTax(subtotalExcludingTax());
110 | }
111 |
112 | @NonNull
113 | @JsonProperty("subtotalIncludingVAT")
114 | public Money subtotalIncludingTax() {
115 | return valueAddedTax().addTax(subtotalExcludingTax());
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/domain/model/OrderItemId.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.domain.model;
2 |
3 | import net.pkhapps.ddd.shared.domain.base.DomainObjectId;
4 |
5 | public class OrderItemId extends DomainObjectId {
6 | public OrderItemId(String uuid) {
7 | super(uuid);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/domain/model/OrderRepository.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.domain.model;
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository;
4 |
5 | public interface OrderRepository extends JpaRepository {
6 | }
7 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/domain/model/OrderState.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.domain.model;
2 |
3 | public enum OrderState {
4 | RECEIVED, PROCESSING, CANCELLED, PROCESSED
5 | }
6 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/domain/model/OrderStateChange.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.domain.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import net.pkhapps.ddd.shared.domain.base.ValueObject;
5 | import org.springframework.lang.NonNull;
6 |
7 | import javax.persistence.Column;
8 | import javax.persistence.Embeddable;
9 | import javax.persistence.EnumType;
10 | import javax.persistence.Enumerated;
11 | import java.time.Instant;
12 | import java.util.Objects;
13 |
14 | @Embeddable
15 | public class OrderStateChange implements ValueObject {
16 |
17 | @Column(name = "changed_on", nullable = false)
18 | private Instant changedOn;
19 | @Column(name = "state", nullable = false)
20 | @Enumerated(EnumType.STRING)
21 | private OrderState state;
22 |
23 | @SuppressWarnings("unused") // Used by JPA only
24 | private OrderStateChange() {
25 | }
26 |
27 | OrderStateChange(@NonNull Instant changedOn, @NonNull OrderState state) {
28 | this.changedOn = Objects.requireNonNull(changedOn, "changedOn must not be null");
29 | this.state = Objects.requireNonNull(state, "state must not be null");
30 | }
31 |
32 | @NonNull
33 | @JsonProperty("changedOn")
34 | public Instant changedOn() {
35 | return changedOn;
36 | }
37 |
38 | @NonNull
39 | @JsonProperty("state")
40 | public OrderState state() {
41 | return state;
42 | }
43 |
44 | @Override
45 | public boolean equals(Object o) {
46 | if (this == o) return true;
47 | if (o == null || getClass() != o.getClass()) return false;
48 | OrderStateChange that = (OrderStateChange) o;
49 | return Objects.equals(changedOn, that.changedOn) &&
50 | state == that.state;
51 | }
52 |
53 | @Override
54 | public int hashCode() {
55 | return Objects.hash(changedOn, state);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/domain/model/Product.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.domain.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonCreator;
4 | import com.fasterxml.jackson.annotation.JsonProperty;
5 | import net.pkhapps.ddd.shared.domain.base.ValueObject;
6 | import net.pkhapps.ddd.shared.domain.financial.Money;
7 | import net.pkhapps.ddd.shared.domain.financial.VAT;
8 | import org.springframework.lang.NonNull;
9 |
10 | import java.util.Objects;
11 |
12 | public class Product implements ValueObject {
13 |
14 | private final ProductId id;
15 | private final String name;
16 | private final VAT valueAddedTax;
17 | private final Money price;
18 |
19 | @JsonCreator
20 | public Product(@JsonProperty("id") @NonNull ProductId id,
21 | @JsonProperty("name") @NonNull String name,
22 | @JsonProperty("valueAddedTax") @NonNull VAT valueAddedTax,
23 | @JsonProperty("priceExcludingVAT") @NonNull Money price) {
24 | this.id = Objects.requireNonNull(id, "id must not be null");
25 | this.name = Objects.requireNonNull(name, "name must not be null");
26 | this.valueAddedTax = Objects.requireNonNull(valueAddedTax, "valueAddedTax must not be null");
27 | this.price = Objects.requireNonNull(price, "price must not be null");
28 | }
29 |
30 | @NonNull
31 | public ProductId id() {
32 | return id;
33 | }
34 |
35 | @NonNull
36 | public String name() {
37 | return name;
38 | }
39 |
40 | @NonNull
41 | public VAT valueAddedTax() {
42 | return valueAddedTax;
43 | }
44 |
45 | @NonNull
46 | public Money price() {
47 | return price;
48 | }
49 |
50 | @Override
51 | public boolean equals(Object o) {
52 | if (this == o) return true;
53 | if (o == null || getClass() != o.getClass()) return false;
54 | Product product = (Product) o;
55 | return Objects.equals(id, product.id) &&
56 | Objects.equals(name, product.name) &&
57 | Objects.equals(valueAddedTax, product.valueAddedTax) &&
58 | Objects.equals(price, product.price);
59 | }
60 |
61 | @Override
62 | public int hashCode() {
63 | return Objects.hash(id, name, valueAddedTax, price);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/domain/model/ProductId.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.domain.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonCreator;
4 | import net.pkhapps.ddd.shared.domain.base.DomainObjectId;
5 |
6 | public class ProductId extends DomainObjectId {
7 | @JsonCreator
8 | public ProductId(String uuid) {
9 | super(uuid);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/domain/model/RecipientAddress.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.domain.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import net.pkhapps.ddd.shared.domain.geo.Address;
5 | import net.pkhapps.ddd.shared.domain.geo.CityName;
6 | import net.pkhapps.ddd.shared.domain.geo.Country;
7 | import net.pkhapps.ddd.shared.domain.geo.PostalCode;
8 | import org.springframework.lang.NonNull;
9 | import org.springframework.lang.Nullable;
10 |
11 | import javax.persistence.Column;
12 | import javax.persistence.Embeddable;
13 | import java.util.Objects;
14 |
15 | @Embeddable
16 | public class RecipientAddress extends Address {
17 |
18 | @Column(name = "recipient_name")
19 | private String name;
20 |
21 | @SuppressWarnings("unused") // Used by JPA only.
22 | protected RecipientAddress() {
23 | }
24 |
25 | public RecipientAddress(@NonNull String name, @NonNull String addressLine1, @Nullable String addressLine2,
26 | @NonNull CityName city, @NonNull PostalCode postalCode, @NonNull Country country) {
27 | super(addressLine1, addressLine2, city, postalCode, country);
28 | this.name = Objects.requireNonNull(name, "name must not be null");
29 | }
30 |
31 | @NonNull
32 | @JsonProperty("name")
33 | public String name() {
34 | return name;
35 | }
36 |
37 | @Override
38 | public boolean equals(Object o) {
39 | if (this == o) return true;
40 | if (o == null || getClass() != o.getClass()) return false;
41 | if (!super.equals(o)) return false;
42 | RecipientAddress that = (RecipientAddress) o;
43 | return Objects.equals(name, that.name);
44 | }
45 |
46 | @Override
47 | public int hashCode() {
48 | return Objects.hash(super.hashCode(), name);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/domain/model/converter/ProductIdConverter.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.domain.model.converter;
2 |
3 | import net.pkhapps.ddd.orders.domain.model.ProductId;
4 |
5 | import javax.persistence.AttributeConverter;
6 | import javax.persistence.Converter;
7 |
8 | @Converter(autoApply = true)
9 | public class ProductIdConverter implements AttributeConverter {
10 | @Override
11 | public String convertToDatabaseColumn(ProductId attribute) {
12 | return attribute == null ? null : attribute.toUUID();
13 | }
14 |
15 | @Override
16 | public ProductId convertToEntityAttribute(String dbData) {
17 | return dbData == null ? null : new ProductId(dbData);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/domain/model/event/OrderCreated.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.domain.model.event;
2 |
3 | import com.fasterxml.jackson.annotation.JsonCreator;
4 | import com.fasterxml.jackson.annotation.JsonProperty;
5 | import net.pkhapps.ddd.orders.domain.model.OrderId;
6 | import net.pkhapps.ddd.shared.domain.base.DomainEvent;
7 | import org.springframework.lang.NonNull;
8 |
9 | import java.time.Instant;
10 | import java.util.Objects;
11 |
12 | public class OrderCreated implements DomainEvent {
13 |
14 | @JsonProperty("orderId")
15 | private final OrderId orderId;
16 | @JsonProperty("occurredOn")
17 | private final Instant occurredOn;
18 |
19 | @JsonCreator
20 | public OrderCreated(@JsonProperty("orderId") @NonNull OrderId orderId,
21 | @JsonProperty("occurredOn") @NonNull Instant occurredOn) {
22 | this.orderId = Objects.requireNonNull(orderId, "orderId must not be null");
23 | this.occurredOn = Objects.requireNonNull(occurredOn, "occurredOn must not be null");
24 | }
25 |
26 | @NonNull
27 | public OrderId orderId() {
28 | return orderId;
29 | }
30 |
31 | @Override
32 | @NonNull
33 | public Instant occurredOn() {
34 | return occurredOn;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/domain/model/event/OrderStateChanged.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.domain.model.event;
2 |
3 | import com.fasterxml.jackson.annotation.JsonCreator;
4 | import com.fasterxml.jackson.annotation.JsonProperty;
5 | import net.pkhapps.ddd.orders.domain.model.OrderId;
6 | import net.pkhapps.ddd.orders.domain.model.OrderState;
7 | import net.pkhapps.ddd.shared.domain.base.DomainEvent;
8 | import org.springframework.lang.NonNull;
9 |
10 | import java.time.Instant;
11 | import java.util.Objects;
12 |
13 | public class OrderStateChanged implements DomainEvent {
14 |
15 | @JsonProperty("orderId")
16 | private final OrderId orderId;
17 | @JsonProperty("state")
18 | private final OrderState state;
19 | @JsonProperty("occurredOn")
20 | private final Instant occurredOn;
21 |
22 | @JsonCreator
23 | public OrderStateChanged(@JsonProperty("orderId") @NonNull OrderId orderId,
24 | @JsonProperty("state") @NonNull OrderState state,
25 | @JsonProperty("occurredOn") @NonNull Instant occurredOn) {
26 | this.orderId = Objects.requireNonNull(orderId, "orderId must not be null");
27 | this.state = Objects.requireNonNull(state, "state must not be null");
28 | this.occurredOn = Objects.requireNonNull(occurredOn, "occurredOn must not be null");
29 | }
30 |
31 | @NonNull
32 | public OrderId orderId() {
33 | return orderId;
34 | }
35 |
36 | @NonNull
37 | public OrderState state() {
38 | return state;
39 | }
40 |
41 | @Override
42 | @NonNull
43 | public Instant occurredOn() {
44 | return occurredOn;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/infra/hibernate/OrderIdType.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.infra.hibernate;
2 |
3 | import net.pkhapps.ddd.orders.domain.model.OrderId;
4 | import net.pkhapps.ddd.shared.infra.hibernate.DomainObjectIdCustomType;
5 | import net.pkhapps.ddd.shared.infra.hibernate.DomainObjectIdTypeDescriptor;
6 |
7 | public class OrderIdType extends DomainObjectIdCustomType {
8 | private static final DomainObjectIdTypeDescriptor TYPE_DESCRIPTOR = new DomainObjectIdTypeDescriptor<>(
9 | OrderId.class, OrderId::new);
10 |
11 | public OrderIdType() {
12 | super(TYPE_DESCRIPTOR);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/infra/hibernate/OrderItemIdType.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.infra.hibernate;
2 |
3 | import net.pkhapps.ddd.orders.domain.model.OrderItemId;
4 | import net.pkhapps.ddd.shared.infra.hibernate.DomainObjectIdCustomType;
5 | import net.pkhapps.ddd.shared.infra.hibernate.DomainObjectIdTypeDescriptor;
6 |
7 | public class OrderItemIdType extends DomainObjectIdCustomType {
8 | private static final DomainObjectIdTypeDescriptor TYPE_DESCRIPTOR = new DomainObjectIdTypeDescriptor<>(
9 | OrderItemId.class, OrderItemId::new);
10 |
11 | public OrderItemIdType() {
12 | super(TYPE_DESCRIPTOR);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/infra/hibernate/package-info.java:
--------------------------------------------------------------------------------
1 | @TypeDefs({
2 | @TypeDef(defaultForType = OrderId.class, typeClass = OrderIdType.class),
3 | @TypeDef(defaultForType = OrderItemId.class, typeClass = OrderItemIdType.class)
4 | })
5 | package net.pkhapps.ddd.orders.infra.hibernate;
6 |
7 | import net.pkhapps.ddd.orders.domain.model.OrderId;
8 | import net.pkhapps.ddd.orders.domain.model.OrderItemId;
9 | import org.hibernate.annotations.TypeDef;
10 | import org.hibernate.annotations.TypeDefs;
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/rest/client/ProductCatalogClient.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.rest.client;
2 |
3 | import net.pkhapps.ddd.orders.application.ProductCatalog;
4 | import net.pkhapps.ddd.orders.domain.model.Product;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.beans.factory.annotation.Value;
8 | import org.springframework.core.ParameterizedTypeReference;
9 | import org.springframework.http.HttpMethod;
10 | import org.springframework.http.client.SimpleClientHttpRequestFactory;
11 | import org.springframework.stereotype.Service;
12 | import org.springframework.web.client.RestTemplate;
13 | import org.springframework.web.util.UriComponentsBuilder;
14 |
15 | import java.util.Collections;
16 | import java.util.List;
17 |
18 | @Service
19 | class ProductCatalogClient implements ProductCatalog {
20 |
21 | private static final Logger LOGGER = LoggerFactory.getLogger(ProductCatalogClient.class);
22 |
23 | private final RestTemplate restTemplate;
24 | private final String serverUrl;
25 |
26 | ProductCatalogClient(@Value("${app.product-catalog.url}") String serverUrl,
27 | @Value("${app.product-catalog.connect-timeout-ms}") int connectTimeout,
28 | @Value("${app.product-catalog.read-timeout-ms}") int readTimeout) {
29 | this.serverUrl = serverUrl;
30 | restTemplate = new RestTemplate();
31 | var requestFactory = new SimpleClientHttpRequestFactory();
32 | // Never ever do a remote call without a finite timeout!
33 | requestFactory.setConnectTimeout(connectTimeout);
34 | requestFactory.setReadTimeout(readTimeout);
35 | restTemplate.setRequestFactory(requestFactory);
36 | }
37 |
38 | private UriComponentsBuilder uri() {
39 | return UriComponentsBuilder.fromUriString(serverUrl);
40 | }
41 |
42 | @Override
43 | public List findAll() {
44 | try {
45 | return restTemplate.exchange(uri().path("/api/products").build().toUri(), HttpMethod.GET, null,
46 | new ParameterizedTypeReference>() {
47 | }).getBody();
48 | } catch (Exception ex) {
49 | LOGGER.error("Error retrieving products", ex);
50 | return Collections.emptyList();
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/rest/controller/OrderCatalogController.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.rest.controller;
2 |
3 | import net.pkhapps.ddd.orders.application.OrderCatalog;
4 | import net.pkhapps.ddd.orders.domain.model.Order;
5 | import net.pkhapps.ddd.orders.domain.model.OrderId;
6 | import org.springframework.http.ResponseEntity;
7 | import org.springframework.web.bind.annotation.*;
8 |
9 | import java.util.List;
10 |
11 | @RestController
12 | @RequestMapping("/api/orders")
13 | class OrderCatalogController {
14 |
15 | private final OrderCatalog orderCatalog;
16 |
17 | OrderCatalogController(OrderCatalog orderCatalog) {
18 | this.orderCatalog = orderCatalog;
19 | }
20 |
21 | @GetMapping("/{id}")
22 | public ResponseEntity findById(@PathVariable("id") String orderId) {
23 | return orderCatalog.findById(new OrderId(orderId))
24 | .map(ResponseEntity::ok)
25 | .orElse(ResponseEntity.notFound().build());
26 | }
27 |
28 | @PutMapping("/{id}/startProcessing")
29 | public void startProcessing(@PathVariable("id") String orderId) {
30 | orderCatalog.startProcessing(new OrderId(orderId));
31 | }
32 |
33 | @PutMapping("/{id}/finishProcessing")
34 | public void finishProcessing(@PathVariable("id") String orderId) {
35 | orderCatalog.finishProcessing(new OrderId(orderId));
36 | }
37 |
38 | @GetMapping
39 | public List findAll() {
40 | return orderCatalog.findAll();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/ui/OrderBrowserView.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.ui;
2 |
3 | import com.vaadin.flow.component.Html;
4 | import com.vaadin.flow.component.button.Button;
5 | import com.vaadin.flow.component.grid.Grid;
6 | import com.vaadin.flow.component.orderedlayout.FlexLayout;
7 | import com.vaadin.flow.component.orderedlayout.VerticalLayout;
8 | import com.vaadin.flow.data.renderer.ComponentRenderer;
9 | import com.vaadin.flow.router.PageTitle;
10 | import com.vaadin.flow.router.Route;
11 | import net.pkhapps.ddd.orders.application.OrderCatalog;
12 | import net.pkhapps.ddd.orders.domain.model.Order;
13 | import net.pkhapps.ddd.orders.domain.model.OrderId;
14 |
15 | @Route("")
16 | @PageTitle("Order Browser")
17 | public class OrderBrowserView extends VerticalLayout {
18 |
19 | private final OrderCatalog orderCatalog;
20 | private final Grid ordersGrid;
21 |
22 | public OrderBrowserView(OrderCatalog orderCatalog) {
23 | this.orderCatalog = orderCatalog;
24 |
25 | setSizeFull();
26 |
27 | var title = new Html("Order Browser
");
28 | add(title);
29 |
30 | ordersGrid = new Grid<>();
31 | ordersGrid.addColumn(Order::orderedOn).setHeader("Date and Time");
32 | ordersGrid.addColumn(Order::state).setHeader("State");
33 | ordersGrid.addColumn(Order::currency).setHeader("Currency");
34 | ordersGrid.addColumn(Order::totalExcludingTax).setHeader("Total (excl.VAT)");
35 | ordersGrid.addColumn(Order::totalTax).setHeader("VAT");
36 | ordersGrid.addColumn(Order::totalIncludingTax).setHeader("Total (incl.VAT)");
37 | ordersGrid.addColumn(new ComponentRenderer<>(order -> new Button("Details", evt -> showOrder(order.id()))));
38 | add(ordersGrid);
39 |
40 | var createOrder = new Button("Create Order", et -> createOrder());
41 | createOrder.getElement().getThemeList().set("primary", true);
42 | var refresh = new Button("Refresh", evt -> refreshOrders());
43 |
44 | var buttons = new FlexLayout(refresh, createOrder);
45 | buttons.setJustifyContentMode(JustifyContentMode.BETWEEN);
46 | buttons.setWidth("100%");
47 | add(buttons);
48 |
49 | refreshOrders();
50 | }
51 |
52 | private void refreshOrders() {
53 | ordersGrid.setItems(orderCatalog.findAll());
54 | }
55 |
56 | private void createOrder() {
57 | getUI().ifPresent(ui -> ui.navigate(CreateOrderView.class));
58 | }
59 |
60 | private void showOrder(OrderId orderId) {
61 | getUI().ifPresent(ui -> ui.navigate(OrderDetailsView.class, orderId.toUUID()));
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/ui/OrderDetailsView.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.ui;
2 |
3 | import com.vaadin.flow.component.Html;
4 | import com.vaadin.flow.component.Text;
5 | import com.vaadin.flow.component.formlayout.FormLayout;
6 | import com.vaadin.flow.component.grid.Grid;
7 | import com.vaadin.flow.component.orderedlayout.VerticalLayout;
8 | import com.vaadin.flow.component.textfield.TextArea;
9 | import com.vaadin.flow.component.textfield.TextField;
10 | import com.vaadin.flow.router.*;
11 | import net.pkhapps.ddd.orders.application.OrderCatalog;
12 | import net.pkhapps.ddd.orders.domain.model.Order;
13 | import net.pkhapps.ddd.orders.domain.model.OrderId;
14 | import net.pkhapps.ddd.orders.domain.model.OrderItem;
15 | import net.pkhapps.ddd.orders.domain.model.RecipientAddress;
16 |
17 | import java.util.Optional;
18 |
19 | @Route("order")
20 | @PageTitle("Show Order")
21 | public class OrderDetailsView extends VerticalLayout implements HasUrlParameter {
22 |
23 | private final OrderCatalog orderCatalog;
24 |
25 | public OrderDetailsView(OrderCatalog orderCatalog) {
26 | this.orderCatalog = orderCatalog;
27 | setSizeFull();
28 | }
29 |
30 | @Override
31 | public void setParameter(BeforeEvent event, @OptionalParameter String parameter) {
32 | Optional order = Optional.ofNullable(parameter).map(OrderId::new).flatMap(orderCatalog::findById);
33 | if (order.isPresent()) {
34 | showOrder(order.get());
35 | } else {
36 | showNoSuchOrder();
37 | }
38 | }
39 |
40 | private void showOrder(Order order) {
41 | var title = new Html("Order Details
");
42 | add(title);
43 |
44 | var header = new FormLayout();
45 | header.addFormItem(createReadOnlyTextField(order.orderedOn().toString()), "Ordered on");
46 | header.addFormItem(createReadOnlyTextField(order.state().name()), "State");
47 | header.addFormItem(createReadOnlyAddressArea(order.billingAddress()), "Billing Address");
48 | header.addFormItem(createReadOnlyAddressArea(order.shippingAddress()), "Shipping Address");
49 | add(header);
50 |
51 | var items = new Grid();
52 | items.addColumn(OrderItem::itemDescription).setHeader("Description");
53 | items.addColumn(OrderItem::quantity).setHeader("Qty");
54 | items.addColumn(OrderItem::itemPrice).setHeader("Price");
55 | items.addColumn(OrderItem::valueAddedTax).setHeader("VAT");
56 | var subtotalExcludingTax = items.addColumn(OrderItem::subtotalExcludingTax).setHeader("Subtotal excl.VAT");
57 | var subtotalTax = items.addColumn(OrderItem::subtotalTax).setHeader("Subtotal VAT");
58 | var subtotalIncludingTax = items.addColumn(OrderItem::subtotalIncludingTax).setHeader("Subtotal incl.VAT");
59 | items.setItems(order.items());
60 | var footerRow = items.appendFooterRow();
61 | footerRow.getCell(subtotalExcludingTax).setText(order.totalExcludingTax().toString());
62 | footerRow.getCell(subtotalTax).setText(order.totalTax().toString());
63 | footerRow.getCell(subtotalIncludingTax).setText(order.totalIncludingTax().toString());
64 | add(items);
65 | }
66 |
67 | private TextField createReadOnlyTextField(String value) {
68 | var textField = new TextField();
69 | textField.setReadOnly(true);
70 | textField.setValue(value);
71 | return textField;
72 | }
73 |
74 | private TextArea createReadOnlyAddressArea(RecipientAddress address) {
75 | var textArea = new TextArea();
76 | textArea.setHeight("140px");
77 | textArea.setValue(formatAddress(address));
78 | textArea.setReadOnly(true);
79 | return textArea;
80 | }
81 |
82 | private String formatAddress(RecipientAddress address) {
83 | var sb = new StringBuilder();
84 | sb.append(address.name()).append("\n");
85 | sb.append(address.addressLine1()).append("\n");
86 | sb.append(Optional.ofNullable(address.addressLine2()).orElse("")).append("\n");
87 | sb.append(address.postalCode()).append(" ").append(address.city()).append("\n");
88 | sb.append(address.country());
89 | return sb.toString();
90 | }
91 |
92 | private void showNoSuchOrder() {
93 | add(new Text("The order does not exist."));
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/orders/src/main/java/net/pkhapps/ddd/orders/ui/TabContainer.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.orders.ui;
2 |
3 | import com.vaadin.flow.component.Component;
4 | import com.vaadin.flow.component.html.Div;
5 | import com.vaadin.flow.component.tabs.Tab;
6 | import com.vaadin.flow.component.tabs.Tabs;
7 |
8 | import java.util.HashMap;
9 | import java.util.Map;
10 |
11 | public class TabContainer extends Div {
12 |
13 | private Map contentMap = new HashMap<>();
14 | private Tabs tabs;
15 |
16 | public TabContainer(Tabs tabs) {
17 | this.tabs = tabs;
18 | tabs.addSelectedChangeListener(event -> showTabContent(tabs.getSelectedTab()));
19 | }
20 |
21 | public void addTab(Tab tab, Component content) {
22 | contentMap.put(tab, content);
23 | }
24 |
25 | public void addTab(String label, Component content) {
26 | var tab = new Tab(label);
27 | tabs.add(tab);
28 | addTab(tab, content);
29 | if (tabs.getSelectedTab() == tab) {
30 | showTabContent(tab);
31 | }
32 | }
33 |
34 | private void showTabContent(Tab tab) {
35 | removeAll();
36 | var content = contentMap.get(tab);
37 | if (content != null) {
38 | add(content);
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/orders/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.h2.console.enabled=true
2 | spring.h2.console.path=/h2-console
3 | spring.datasource.url=jdbc:h2:mem:orders;DB_CLOSE_DELAY=-1.
4 | server.port=9002
5 |
6 | app.product-catalog.url=http://localhost:9001
7 | app.product-catalog.connect-timeout-ms=1000
8 | app.product-catalog.read-timeout-ms=5000
9 | app.currency-conversion.enabled=true
10 | logging.level.net.pkhapps.ddd=debug
11 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | net.pkhapps.ddd
8 | master-pom
9 | 1.0-SNAPSHOT
10 | pom
11 |
12 |
13 | product-catalog
14 | shipping
15 | invoicing
16 | shared-kernel
17 | orders
18 |
19 |
20 |
21 |
22 | org.springframework.boot
23 | spring-boot-starter-parent
24 | 2.1.0.RELEASE
25 |
26 |
27 |
28 | 11
29 | 11.0.2
30 | 11.0.2
31 |
32 |
33 |
34 |
35 |
36 | com.vaadin
37 | vaadin-bom
38 | ${vaadin.version}
39 | import
40 | pom
41 |
42 |
43 | com.vaadin
44 | vaadin-spring-boot-starter
45 | ${vaadin-spring.version}
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/product-catalog/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | master-pom
7 | net.pkhapps.ddd
8 | 1.0-SNAPSHOT
9 |
10 | 4.0.0
11 |
12 | product-catalog
13 |
14 |
15 |
16 | net.pkhapps.ddd
17 | shared-kernel
18 | ${project.version}
19 |
20 |
21 | org.springframework.boot
22 | spring-boot-starter-web
23 |
24 |
25 | com.h2database
26 | h2
27 |
28 |
29 |
30 |
31 |
32 |
33 | org.springframework.boot
34 | spring-boot-maven-plugin
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/product-catalog/src/main/java/net/pkhapps/ddd/productcatalog/DataGenerator.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.productcatalog;
2 |
3 | import net.pkhapps.ddd.productcatalog.domain.model.Product;
4 | import net.pkhapps.ddd.productcatalog.domain.model.ProductRepository;
5 | import net.pkhapps.ddd.shared.domain.financial.Currency;
6 | import net.pkhapps.ddd.shared.domain.financial.Money;
7 | import net.pkhapps.ddd.shared.domain.financial.VAT;
8 | import org.springframework.stereotype.Component;
9 | import org.springframework.transaction.annotation.Transactional;
10 |
11 | import javax.annotation.PostConstruct;
12 | import java.util.ArrayList;
13 |
14 | @Component
15 | class DataGenerator {
16 |
17 | private final ProductRepository productRepository;
18 |
19 | DataGenerator(ProductRepository productRepository) {
20 | this.productRepository = productRepository;
21 | }
22 |
23 | @PostConstruct
24 | @Transactional
25 | public void generateData() {
26 | var products = new ArrayList();
27 | products.add(createProduct("Flashlight L", "A large flashlight", new VAT(24), new Money(Currency.EUR, 5642)));
28 | products.add(createProduct("Flashlight M", "A medium flashlight", new VAT(24), new Money(Currency.EUR, 4029)));
29 | products.add(createProduct("Flashlight S", "A small flashlight", new VAT(24), new Money(Currency.EUR, 2416)));
30 | productRepository.saveAll(products);
31 | }
32 |
33 | private Product createProduct(String name, String description, VAT vat, Money price) {
34 | var product = new Product(name, price, vat);
35 | product.setDescription(description);
36 | return product;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/product-catalog/src/main/java/net/pkhapps/ddd/productcatalog/ProductCatalogApp.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.productcatalog;
2 |
3 | import net.pkhapps.ddd.shared.SharedConfiguration;
4 | import org.springframework.boot.SpringApplication;
5 | import org.springframework.boot.autoconfigure.SpringBootApplication;
6 | import org.springframework.boot.autoconfigure.domain.EntityScan;
7 | import org.springframework.context.annotation.Import;
8 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
9 |
10 | @SpringBootApplication
11 | @EnableJpaRepositories
12 | @EntityScan
13 | @Import(SharedConfiguration.class)
14 | public class ProductCatalogApp {
15 |
16 | public static void main(String[] args) {
17 | SpringApplication.run(ProductCatalogApp.class, args);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/product-catalog/src/main/java/net/pkhapps/ddd/productcatalog/application/ProductCatalog.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.productcatalog.application;
2 |
3 | import net.pkhapps.ddd.productcatalog.domain.model.Product;
4 | import net.pkhapps.ddd.productcatalog.domain.model.ProductId;
5 | import net.pkhapps.ddd.productcatalog.domain.model.ProductRepository;
6 | import org.springframework.lang.NonNull;
7 | import org.springframework.stereotype.Service;
8 | import org.springframework.transaction.annotation.Propagation;
9 | import org.springframework.transaction.annotation.Transactional;
10 |
11 | import java.util.List;
12 | import java.util.Objects;
13 | import java.util.Optional;
14 |
15 | /**
16 | * Application service for browsing the product catalog.
17 | */
18 | @Service
19 | @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
20 | public class ProductCatalog {
21 |
22 | private final ProductRepository productRepository;
23 |
24 | ProductCatalog(ProductRepository productRepository) {
25 | this.productRepository = productRepository;
26 | }
27 |
28 | @NonNull
29 | public Optional findById(@NonNull ProductId productId) {
30 | Objects.requireNonNull(productId, "productId must not be null");
31 | return productRepository.findById(productId);
32 | }
33 |
34 | // Please note: in a real-world application you would use pagination for all results that don't have an upper-bound.
35 | // However, to save time, we're just returning everything in a single list in this example.
36 |
37 | @NonNull
38 | public List findAll() {
39 | return productRepository.findActive();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/product-catalog/src/main/java/net/pkhapps/ddd/productcatalog/domain/model/Product.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.productcatalog.domain.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import net.pkhapps.ddd.shared.domain.base.AbstractAggregateRoot;
5 | import net.pkhapps.ddd.shared.domain.base.ConcurrencySafeDomainObject;
6 | import net.pkhapps.ddd.shared.domain.base.DeletableDomainObject;
7 | import net.pkhapps.ddd.shared.domain.base.DomainObjectId;
8 | import net.pkhapps.ddd.shared.domain.financial.Currency;
9 | import net.pkhapps.ddd.shared.domain.financial.Money;
10 | import net.pkhapps.ddd.shared.domain.financial.VAT;
11 | import org.springframework.lang.NonNull;
12 | import org.springframework.lang.Nullable;
13 |
14 | import javax.persistence.*;
15 | import java.util.Objects;
16 |
17 | /**
18 | * Aggregate root representing a product in the product catalog.
19 | */
20 | @Entity
21 | @Table(name = "products")
22 | public class Product extends AbstractAggregateRoot implements DeletableDomainObject,
23 | ConcurrencySafeDomainObject {
24 |
25 | @Version
26 | private Long version;
27 |
28 | @Column(name = "name", nullable = false)
29 | private String name;
30 |
31 | @Column(name = "description")
32 | private String description;
33 |
34 | @Column(name = "price_currency", nullable = false)
35 | @Enumerated(EnumType.STRING)
36 | private Currency priceCurrency;
37 |
38 | @Column(name = "price_value", nullable = false)
39 | private Integer priceValue;
40 |
41 | @Column(name = "vat", nullable = false)
42 | private VAT vat;
43 |
44 | @Column(name = "deleted", nullable = false)
45 | private boolean deleted = false;
46 |
47 | @SuppressWarnings("unused") // Used by JPA only
48 | private Product() {
49 | }
50 |
51 | public Product(@NonNull String name, @NonNull Money price, @NonNull VAT vat) {
52 | super(DomainObjectId.randomId(ProductId.class));
53 | setName(name);
54 | setPrice(price);
55 | setVAT(vat);
56 | }
57 |
58 | @NonNull
59 | @JsonProperty("name")
60 | public String name() {
61 | return name;
62 | }
63 |
64 | private void setName(@NonNull String name) {
65 | this.name = Objects.requireNonNull(name, "name must not be null");
66 | }
67 |
68 | @Nullable
69 | @JsonProperty("description")
70 | public String description() {
71 | return description;
72 | }
73 |
74 | public void setDescription(@Nullable String description) {
75 | this.description = description;
76 | }
77 |
78 | @NonNull
79 | @JsonProperty("priceExcludingVAT")
80 | public Money price() {
81 | return Money.valueOf(priceCurrency, priceValue);
82 | }
83 |
84 | @NonNull
85 | @JsonProperty("priceIncludingVAT")
86 | public Money priceIncludingVAT() {
87 | return vat().addTax(price());
88 | }
89 |
90 | private void setPrice(@NonNull Money price) {
91 | Objects.requireNonNull(price, "price must not be null");
92 | priceCurrency = price.currency();
93 | priceValue = price.fixedPointAmount();
94 | }
95 |
96 | @NonNull
97 | @JsonProperty("valueAddedTax")
98 | public VAT vat() {
99 | return vat;
100 | }
101 |
102 | private void setVAT(@NonNull VAT vat) {
103 | this.vat = Objects.requireNonNull(vat);
104 | }
105 |
106 | @Override
107 | @Nullable
108 | public Long version() {
109 | return version;
110 | }
111 |
112 | @Override
113 | @JsonProperty("isDeleted")
114 | public boolean isDeleted() {
115 | return deleted;
116 | }
117 |
118 | @Override
119 | public void delete() {
120 | this.deleted = true;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/product-catalog/src/main/java/net/pkhapps/ddd/productcatalog/domain/model/ProductId.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.productcatalog.domain.model;
2 |
3 | import net.pkhapps.ddd.shared.domain.base.DomainObjectId;
4 |
5 | /**
6 | * Value object representing a {@link Product} ID.
7 | */
8 | public class ProductId extends DomainObjectId {
9 | public ProductId(String uuid) {
10 | super(uuid);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/product-catalog/src/main/java/net/pkhapps/ddd/productcatalog/domain/model/ProductRepository.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.productcatalog.domain.model;
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository;
4 | import org.springframework.data.jpa.repository.Query;
5 |
6 | import java.util.List;
7 |
8 | /**
9 | * Repository of {@link Product}s.
10 | */
11 | public interface ProductRepository extends JpaRepository {
12 |
13 | @Query("select p from Product p where p.deleted = false order by p.name")
14 | List findActive();
15 | }
16 |
--------------------------------------------------------------------------------
/product-catalog/src/main/java/net/pkhapps/ddd/productcatalog/infra/hibernate/ProductIdType.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.productcatalog.infra.hibernate;
2 |
3 | import net.pkhapps.ddd.productcatalog.domain.model.ProductId;
4 | import net.pkhapps.ddd.shared.infra.hibernate.DomainObjectIdCustomType;
5 | import net.pkhapps.ddd.shared.infra.hibernate.DomainObjectIdTypeDescriptor;
6 |
7 | /**
8 | * Hibernate custom type for {@link ProductId}.
9 | */
10 | public class ProductIdType extends DomainObjectIdCustomType {
11 |
12 | private static final DomainObjectIdTypeDescriptor TYPE_DESCRIPTOR = new DomainObjectIdTypeDescriptor<>(
13 | ProductId.class, ProductId::new);
14 |
15 | public ProductIdType() {
16 | super(TYPE_DESCRIPTOR);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/product-catalog/src/main/java/net/pkhapps/ddd/productcatalog/infra/hibernate/package-info.java:
--------------------------------------------------------------------------------
1 | @TypeDef(defaultForType = ProductId.class, typeClass = ProductIdType.class)
2 | package net.pkhapps.ddd.productcatalog.infra.hibernate;
3 |
4 | import net.pkhapps.ddd.productcatalog.domain.model.ProductId;
5 | import org.hibernate.annotations.TypeDef;
--------------------------------------------------------------------------------
/product-catalog/src/main/java/net/pkhapps/ddd/productcatalog/rest/ProductCatalogController.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.productcatalog.rest;
2 |
3 | import net.pkhapps.ddd.productcatalog.application.ProductCatalog;
4 | import net.pkhapps.ddd.productcatalog.domain.model.Product;
5 | import net.pkhapps.ddd.productcatalog.domain.model.ProductId;
6 | import org.springframework.http.ResponseEntity;
7 | import org.springframework.web.bind.annotation.GetMapping;
8 | import org.springframework.web.bind.annotation.PathVariable;
9 | import org.springframework.web.bind.annotation.RequestMapping;
10 | import org.springframework.web.bind.annotation.RestController;
11 |
12 | import java.util.List;
13 |
14 | /**
15 | * Controller for making {@link ProductCatalog} available through REST.
16 | */
17 | @RestController
18 | @RequestMapping("/api/products")
19 | class ProductCatalogController {
20 |
21 | private final ProductCatalog productCatalog;
22 |
23 | ProductCatalogController(ProductCatalog productCatalog) {
24 | this.productCatalog = productCatalog;
25 | }
26 |
27 | // Please note: in a real-world application it would be better to have separate DTO classes that are serialized
28 | // to JSON. However, to save time, we're using the entity classes directly in this example.
29 |
30 | @GetMapping("/{id}")
31 | public ResponseEntity findById(@PathVariable("id") String productId) {
32 | return productCatalog.findById(new ProductId(productId))
33 | .map(ResponseEntity::ok)
34 | .orElse(ResponseEntity.notFound().build());
35 | }
36 |
37 | @GetMapping
38 | public List findAll() {
39 | return productCatalog.findAll();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/product-catalog/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.h2.console.enabled=true
2 | spring.h2.console.path=/h2-console
3 | spring.datasource.url=jdbc:h2:mem:product-catalog;DB_CLOSE_DELAY=-1.
4 | server.port=9001
5 | logging.level.net.pkhapps.ddd=debug
6 |
--------------------------------------------------------------------------------
/shared-kernel/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | master-pom
7 | net.pkhapps.ddd
8 | 1.0-SNAPSHOT
9 |
10 | 4.0.0
11 |
12 | shared-kernel
13 |
14 |
15 |
16 | org.springframework.boot
17 | spring-boot-starter-data-jpa
18 |
19 |
20 | org.springframework.boot
21 | spring-boot-starter-web
22 |
23 |
24 | com.fasterxml.jackson.core
25 | jackson-databind
26 |
27 |
28 | com.vaadin
29 | vaadin-core
30 | provided
31 | true
32 |
33 |
34 |
35 | junit
36 | junit
37 | test
38 |
39 |
40 | org.assertj
41 | assertj-core
42 | test
43 |
44 |
45 | org.mockito
46 | mockito-core
47 | test
48 |
49 |
50 |
--------------------------------------------------------------------------------
/shared-kernel/src/main/java/net/pkhapps/ddd/shared/SharedConfiguration.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.shared;
2 |
3 | import org.springframework.boot.autoconfigure.domain.EntityScan;
4 | import org.springframework.context.annotation.ComponentScan;
5 | import org.springframework.context.annotation.Configuration;
6 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
7 | import org.springframework.scheduling.annotation.EnableScheduling;
8 |
9 | @Configuration
10 | @ComponentScan
11 | @EntityScan
12 | @EnableJpaRepositories
13 | @EnableScheduling
14 | public class SharedConfiguration {
15 | }
16 |
--------------------------------------------------------------------------------
/shared-kernel/src/main/java/net/pkhapps/ddd/shared/domain/base/AbstractAggregateRoot.java:
--------------------------------------------------------------------------------
1 | package net.pkhapps.ddd.shared.domain.base;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnore;
4 | import org.springframework.data.domain.AfterDomainEventPublication;
5 | import org.springframework.data.domain.DomainEvents;
6 | import org.springframework.lang.NonNull;
7 |
8 | import javax.persistence.MappedSuperclass;
9 | import javax.persistence.Transient;
10 | import java.util.*;
11 |
12 | /**
13 | * Base class for aggregate roots.
14 | *
15 | * @param the aggregate root ID type.
16 | */
17 | @MappedSuperclass
18 | public abstract class AbstractAggregateRoot extends AbstractEntity {
19 |
20 | @Transient
21 | @JsonIgnore
22 | private List domainEvents = new ArrayList<>();
23 |
24 | /**
25 | * Default constructor
26 | */
27 | protected AbstractAggregateRoot() {
28 | }
29 |
30 | /**
31 | * Copy constructor. Please note that any registered domain events are not copied.
32 | *
33 | * @param source the aggregate root to copy from.
34 | */
35 | protected AbstractAggregateRoot(@NonNull AbstractAggregateRoot source) {
36 | super(source);
37 | }
38 |
39 | /**
40 | * Constructor for creating new aggregate roots.
41 | *
42 | * @param id the ID to assign to the aggregate root.
43 | */
44 | protected AbstractAggregateRoot(ID id) {
45 | super(id);
46 | }
47 |
48 | /**
49 | * Registers the given domain event to be published when the aggregate root is persisted.
50 | *
51 | * @param event the event to register.
52 | */
53 | @NonNull
54 | protected void registerEvent(@NonNull DomainEvent event) {
55 | Objects.requireNonNull(event, "event must not be null");
56 | this.domainEvents.add(event);
57 | }
58 |
59 | /**
60 | * Called by the persistence framework to clear all registered domain events once they have been published.
61 | */
62 | @AfterDomainEventPublication
63 | protected void clearDomainEvents() {
64 | this.domainEvents.clear();
65 | }
66 |
67 | /**
68 | * Returns all domain events that have been registered for publication. Intended to be used by the persistence
69 | * framework only.
70 | */
71 | @DomainEvents
72 | protected Collection