Add Domain Core Classes And Common Domain Module.

This commit is contained in:
Ali CANLI
2022-07-09 20:33:57 +03:00
parent d17282a7bb
commit 3f5b9db4f5
19 changed files with 720 additions and 0 deletions

31
README.md Normal file
View File

@@ -0,0 +1,31 @@
<p align="center">
<img src="img/diagram.png" alt="ci" width="1000" class="center"/>
</p>
# What we are doing here ?
- Hexagonal (Clean) Architecture -> Port & Adapter Style
- Domain Driven Desing (DDD)
- SAGA Pattern : process & rollback ( compensating transactions )
- Outbox Pattern : Pulling Outbox Table With Scheduler , Saga Status
- Cover Failure Scerinarios :
- Ensure idempotency using outbox table in each service
- Prevent concurrency issues with optimistic looks & DB constaints
- Kepp updating saga and order status for each operation
- CQRS Pattern : Materialized view & Event Sourcing
- Relational Database : for ACID and distributed transactional
- Kafka Messaging Systems for CQRS desing and Microservices Communication
- Kubernetes And GKE ( Google Kubernetes Engine )

View File

@@ -0,0 +1,12 @@
package com.food.order.domain.exception;
public class DomainException extends RuntimeException {
public DomainException(String message) {
super(message);
}
public DomainException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -8,6 +8,8 @@ public class Money {
private final BigDecimal amount;
public static final Money ZERO = new Money(BigDecimal.ZERO);
public Money(BigDecimal amount) {
this.amount = amount;
}

View File

@@ -0,0 +1,7 @@
package com.food.order.system.domain.entity;
import com.food.order.domain.entity.AggregateRoot;
import com.food.order.domain.valueobject.CustomerId;
public class Customer extends AggregateRoot<CustomerId> {
}

View File

@@ -0,0 +1,225 @@
package com.food.order.system.domain.entity;
import com.food.order.domain.entity.AggregateRoot;
import com.food.order.domain.valueobject.*;
import com.food.order.system.domain.exception.OrderDomainException;
import com.food.order.system.domain.valueobject.OrderItemId;
import com.food.order.system.domain.valueobject.StreetAddress;
import com.food.order.system.domain.valueobject.TrackingId;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
public class Order extends AggregateRoot<OrderId> {
private final CustomerId customerId;
private final RestaurantId restaurantId;
private final StreetAddress deliveryAddress;
private final Money price;
private final List<OrderItem> items;
private TrackingId trackingId;
private OrderStatus status;
private List<String> failureMessages;
public void initializeOrder(){
setId(new OrderId(UUID.randomUUID()));
trackingId = new TrackingId(UUID.randomUUID());
status = OrderStatus.PENDING;
initializeOrderItems();
}
public void pay(){
if(!Objects.equals(status, OrderStatus.PENDING))
throw new OrderDomainException("Order status is not pending !");
status = OrderStatus.PAID;
}
public void approve(){
if(!Objects.equals(status, OrderStatus.PAID))
throw new OrderDomainException("Order status is not paid !");
status = OrderStatus.APPROVED;
}
public void initCancel(List<String> failureMessages){
if(!Objects.equals(status, OrderStatus.PAID))
throw new OrderDomainException("Order status is not correct for initCancel operation !");
status = OrderStatus.CANCELLED;
updateFailureMessages(failureMessages);
}
public void cancel(List<String> failureMessages){
if(Objects.equals(status, OrderStatus.CANCELLING) || Objects.equals(status, OrderStatus.PENDING))
throw new OrderDomainException("Order status is not correct for cancel operation !");
status = OrderStatus.CANCELLED;
updateFailureMessages(failureMessages);
}
private void updateFailureMessages(List<String> failureMessages) {
if (Objects.nonNull(this.failureMessages) && Objects.nonNull(failureMessages)) {
this.failureMessages.addAll(failureMessages.stream().filter(Objects::nonNull).toList());
} else {
this.failureMessages = failureMessages;
}
}
public void validateOrder(){
validateInitialOrder();
validateTotalPrice();
validateItemsPrice();
}
private void validateItemsPrice() {
Money orderItemsTotal = items
.stream()
.map(orderItem -> {
validateItemPrice(orderItem);
return orderItem.getSubTotal();
})
.reduce(Money.ZERO, Money::add);
if(!Objects.equals(orderItemsTotal, price)){
throw new OrderDomainException("Order total price is not equal to the sum of order items prices");
}
}
private void validateItemPrice(OrderItem orderItem) {
if(!orderItem.isPriceValid()){
throw new OrderDomainException("Order item price is not valid");
}
}
private void validateTotalPrice() {
if(Objects.nonNull(price) && !price.isGreaterThanZero()){
throw new OrderDomainException("Total price cannot be less than zero");
}
}
private void validateInitialOrder() {
if (Objects.nonNull(status) && Objects.nonNull(getId()))
throw new OrderDomainException("Order is not in correct state to be initialized");
}
private void initializeOrderItems() {
long itemId = 1;
for (OrderItem item : items) {
item.initializeOrderItem(super.getId(), new OrderItemId(itemId++));
}
}
private Order(Builder builder) {
super.setId(builder.orderId);
customerId = builder.customerId;
restaurantId = builder.restaurantId;
deliveryAddress = builder.deliveryAddress;
price = builder.price;
items = builder.items;
trackingId = builder.trackingId;
status = builder.status;
failureMessages = builder.failureMessages;
}
public static Builder builder() {
return new Builder();
}
public CustomerId getCustomerId() {
return customerId;
}
public RestaurantId getRestaurantId() {
return restaurantId;
}
public StreetAddress getDeliveryAddress() {
return deliveryAddress;
}
public Money getPrice() {
return price;
}
public List<OrderItem> getItems() {
return items;
}
public TrackingId getTrackingId() {
return trackingId;
}
public OrderStatus getStatus() {
return status;
}
public List<String> getFailureMessages() {
return failureMessages;
}
public static final class Builder {
private OrderId orderId;
private CustomerId customerId;
private RestaurantId restaurantId;
private StreetAddress deliveryAddress;
private Money price;
private List<OrderItem> items;
private TrackingId trackingId;
private OrderStatus status;
private List<String> failureMessages;
private Builder() {
}
public Builder orderId(OrderId val) {
orderId = val;
return this;
}
public Builder customerId(CustomerId val) {
customerId = val;
return this;
}
public Builder restaurantId(RestaurantId val) {
restaurantId = val;
return this;
}
public Builder deliveryAddress(StreetAddress val) {
deliveryAddress = val;
return this;
}
public Builder price(Money val) {
price = val;
return this;
}
public Builder items(List<OrderItem> val) {
items = val;
return this;
}
public Builder trackingId(TrackingId val) {
trackingId = val;
return this;
}
public Builder status(OrderStatus val) {
status = val;
return this;
}
public Builder failureMessages(List<String> val) {
failureMessages = val;
return this;
}
public Order build() {
return new Order(this);
}
}
}

View File

@@ -0,0 +1,101 @@
package com.food.order.system.domain.entity;
import com.food.order.domain.entity.BaseEntity;
import com.food.order.domain.valueobject.Money;
import com.food.order.domain.valueobject.OrderId;
import com.food.order.system.domain.valueobject.OrderItemId;
public class OrderItem extends BaseEntity<OrderItemId> {
private OrderId orderId;
private final Product product;
private final int quantity;
private final Money price;
private final Money subTotal;
void initializeOrderItem(OrderId orderId, OrderItemId orderItemId) {
this.orderId = orderId;
super.setId(orderItemId);
}
boolean isPriceValid() {
return price.isGreaterThanZero() &&
price.equals(product.getPrice()) &&
price.multiply(quantity).equals(subTotal);
}
private OrderItem(Builder builder) {
super.setId(builder.orderItemId);
product = builder.product;
quantity = builder.quantity;
price = builder.price;
subTotal = builder.subTotal;
}
public static Builder builder() {
return new Builder();
}
public OrderId getOrderId() {
return orderId;
}
public Product getProduct() {
return product;
}
public int getQuantity() {
return quantity;
}
public Money getPrice() {
return price;
}
public Money getSubTotal() {
return subTotal;
}
public static final class Builder {
private OrderItemId orderItemId;
private Product product;
private int quantity;
private Money price;
private Money subTotal;
private Builder() {
}
public Builder orderItemId(OrderItemId val) {
orderItemId = val;
return this;
}
public Builder product(Product val) {
product = val;
return this;
}
public Builder quantity(int val) {
quantity = val;
return this;
}
public Builder price(Money val) {
price = val;
return this;
}
public Builder subTotal(Money val) {
subTotal = val;
return this;
}
public OrderItem build() {
return new OrderItem(this);
}
}
}

View File

@@ -0,0 +1,31 @@
package com.food.order.system.domain.entity;
import com.food.order.domain.entity.BaseEntity;
import com.food.order.domain.valueobject.Money;
import com.food.order.domain.valueobject.ProductId;
public class Product extends BaseEntity<ProductId> {
private String name;
private Money price;
public Product(ProductId id, String name, Money price) {
super.setId(id);
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public Money getPrice() {
return price;
}
public void updateWithConfirmedNameAndPrice(String name, Money price) {
this.name = name;
this.price = price;
}
}

View File

@@ -0,0 +1,58 @@
package com.food.order.system.domain.entity;
import com.food.order.domain.entity.AggregateRoot;
import com.food.order.domain.valueobject.RestaurantId;
import java.util.List;
public class Restaurant extends AggregateRoot<RestaurantId> {
private final List<Product> products;
private final boolean isActive;
private Restaurant(Builder builder) {
super.setId(builder.restaurantId);
products = builder.products;
isActive = builder.isActive;
}
public static Builder builder() {
return new Builder();
}
public List<Product> getProducts() {
return products;
}
public boolean isActive() {
return isActive;
}
public static final class Builder {
private RestaurantId restaurantId;
private List<Product> products;
private boolean isActive;
private Builder() {
}
public Builder id(RestaurantId val) {
restaurantId = val;
return this;
}
public Builder products(List<Product> val) {
products = val;
return this;
}
public Builder isActive(boolean val) {
isActive = val;
return this;
}
public Restaurant build() {
return new Restaurant(this);
}
}
}

View File

@@ -0,0 +1,12 @@
package com.food.order.system.domain.event;
import com.food.order.system.domain.entity.Order;
import java.time.ZonedDateTime;
public class OrderCancelledEvent extends OrderEvent {
public OrderCancelledEvent(Order order, ZonedDateTime utc) {
super(order, utc);
}
}

View File

@@ -0,0 +1,11 @@
package com.food.order.system.domain.event;
import com.food.order.system.domain.entity.Order;
import java.time.ZonedDateTime;
public class OrderCreatedEvent extends OrderEvent {
public OrderCreatedEvent(Order order, ZonedDateTime createdAt) {
super(order, createdAt);
}
}

View File

@@ -0,0 +1,24 @@
package com.food.order.system.domain.event;
import com.food.order.domain.event.DomainEvent;
import com.food.order.system.domain.entity.Order;
import java.time.ZonedDateTime;
public abstract class OrderEvent implements DomainEvent<Order> {
private final Order order;
private final ZonedDateTime createdAt;
protected OrderEvent(Order order, ZonedDateTime createdAt) {
this.order = order;
this.createdAt = ZonedDateTime.now();
}
public Order getOrder() {
return order;
}
public ZonedDateTime getCreatedAt() {
return createdAt;
}
}

View File

@@ -0,0 +1,13 @@
package com.food.order.system.domain.event;
import com.food.order.system.domain.entity.Order;
import java.time.ZonedDateTime;
public class OrderPaidEvent extends OrderEvent {
public OrderPaidEvent(Order order, ZonedDateTime utc) {
super(order, utc);
}
}

View File

@@ -0,0 +1,14 @@
package com.food.order.system.domain.exception;
import com.food.order.domain.exception.DomainException;
public class OrderDomainException extends DomainException {
public OrderDomainException(String message) {
super(message);
}
public OrderDomainException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,23 @@
package com.food.order.system.domain.service;
import com.food.order.system.domain.entity.Order;
import com.food.order.system.domain.entity.Restaurant;
import com.food.order.system.domain.event.OrderCancelledEvent;
import com.food.order.system.domain.event.OrderCreatedEvent;
import com.food.order.system.domain.event.OrderPaidEvent;
import java.util.List;
public interface OrderDomainService {
OrderCreatedEvent validateAndInitiateOrder(Order order, Restaurant restaurant);
OrderPaidEvent payOrder(Order order);
void approve(Order order);
OrderCancelledEvent cancelOrderPayment(Order order, List<String> failureMessages);
void cancelOrder(Order order, List<String> failureMessages);
}

View File

@@ -0,0 +1,73 @@
package com.food.order.system.domain.service.impl;
import com.food.order.system.domain.entity.Order;
import com.food.order.system.domain.entity.Restaurant;
import com.food.order.system.domain.event.OrderCancelledEvent;
import com.food.order.system.domain.event.OrderCreatedEvent;
import com.food.order.system.domain.event.OrderPaidEvent;
import com.food.order.system.domain.exception.OrderDomainException;
import com.food.order.system.domain.service.OrderDomainService;
import lombok.extern.slf4j.Slf4j;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;
@Slf4j
public class OrderDomainServiceImpl implements OrderDomainService {
private static final String UTC = "UTC";
@Override
public OrderCreatedEvent validateAndInitiateOrder(Order order, Restaurant restaurant) {
validateRestaurant(restaurant);
setOrderProductInformation(order,restaurant);
order.validateOrder();
order.initializeOrder();
log.info("Order with id {} initialize successfully", order.getId().getValue());
return new OrderCreatedEvent(order, ZonedDateTime.now(ZoneId.of(UTC)));
}
private void setOrderProductInformation(Order order, Restaurant restaurant) {
order.getItems()
.forEach(orderItem -> restaurant.getProducts().forEach(restaurantProduct -> {
var currentProduct = orderItem.getProduct();
if(currentProduct.equals(restaurantProduct)){
currentProduct.updateWithConfirmedNameAndPrice(restaurantProduct.getName(),restaurantProduct.getPrice());
}
}));
}
private void validateRestaurant(Restaurant restaurant) {
if (Boolean.FALSE.equals(restaurant.isActive())) {
throw new OrderDomainException("Restaurant is not active, please try again later. " +
"Restaurant id: {} " + restaurant.getId());
}
}
@Override
public OrderPaidEvent payOrder(Order order) {
order.pay();
log.info("Order with id {} paid successfully", order.getId().getValue());
return new OrderPaidEvent(order, ZonedDateTime.now(ZoneId.of(UTC)));
}
@Override
public void approve(Order order) {
order.approve();
log.info("Order with id {} approved successfully", order.getId().getValue());
}
@Override
public OrderCancelledEvent cancelOrderPayment(Order order, List<String> failureMessages) {
order.initCancel(failureMessages);
log.info("Order with id {} cancelled successfully", order.getId().getValue());
return new OrderCancelledEvent(order, ZonedDateTime.now(ZoneId.of(UTC)));
}
@Override
public void cancelOrder(Order order, List<String> failureMessages) {
order.cancel(failureMessages);
log.info("Order with id {} cancelled successfully", order.getId().getValue());
}
}

View File

@@ -0,0 +1,12 @@
package com.food.order.system.domain.valueobject;
import com.food.order.domain.valueobject.BaseId;
import java.util.UUID;
public class OrderItemId extends BaseId<Long> {
public OrderItemId(Long id) {
super(id);
}
}

View File

@@ -0,0 +1,47 @@
package com.food.order.system.domain.valueobject;
import java.util.UUID;
public class StreetAddress {
private final UUID id;
private final String street;
private final String city;
private final String postalCode;
public StreetAddress(UUID id, String street, String city, String postalCode) {
this.id = id;
this.street = street;
this.city = city;
this.postalCode = postalCode;
}
public UUID getId() {
return id;
}
public String getStreet() {
return street;
}
public String getCity() {
return city;
}
public String getPostalCode() {
return postalCode;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof StreetAddress that)) return false;
return id.equals(that.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
}

View File

@@ -0,0 +1,11 @@
package com.food.order.system.domain.valueobject;
import com.food.order.domain.valueobject.BaseId;
import java.util.UUID;
public class TrackingId extends BaseId<UUID> {
public TrackingId(UUID id) {
super(id);
}
}

13
pom.xml
View File

@@ -73,6 +73,19 @@
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
</dependencies>
<build>
<plugins>