9

Domain Driven Design Compromises

 3 years ago
source link: https://sergiuoltean.com/2020/04/01/domain-driven-design-compromises/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Design

Domain Driven Design Compromises

Domain driven design is a great way to build applications. You can isolate the business from the infrastructure and speak with non-technical team members using the ubiquitous language. The domain should be clean from infrastructure, but as we will see in the next lines some compromises need to be made.

photo-1487014679447-9f8336841d58?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2066&q=80

Lombok. Do we use lombok for our entities? Even tough we can generate getters and setters using out IDE I tend to simplify my code and go with lombok. So this is the first compromise that I make.

We want to get as much as we can with less code, so the compromise that I usually make here is to not follow the Data Mapper pattern as is, but I tend to follow shortcuts and apply the Java Persistence Api standard directly on the entities. In this way I get the mapping for free because JPA is a standard and the persistence frameworks should implement it. That is in the case of a relational database. It won’t work with NoSql or other implementations. As an example this would be part of the domain

@Builder
@Data
public class Order {
private Long id;
private Integer amount;
private Integer productCode;
private Status status;
}
public enum Status {
IN_PROGRESS("in_progress"), COMPLETED("completed"), REJECTED("rejected");
private String status;
Status(String status) {
this.status = status;
}
}

and the following is part of the infrastructure. In this particular example this represents the data layer.

@Data
@Entity
public class OrderRow {
@Id
private Integer id;
private Integer amount;
private Integer productCode;
private String status;
}
public class OrderMapper implements Function<OrderRow, Order> {
@Override
public Order apply(OrderRow orderRow) {
return Order.builder()
.id(orderRow.getId())
.amount(orderRow.getAmount())
.productCode(orderRow.getProductCode())
.status(Status.valueOf(orderRow.getStatus()))
.build();
}
}

The above block can be replaced with

@Builder
@Data
@Entity
public class Order {
@Id
private Long id;
private Integer amount;
private Integer productCode;
@Convert(converter = StatusConverter.class)
private Status status;
}
public enum Status {
IN_PROGRESS("in_progress"), COMPLETED("completed"), REJECTED("rejected");
private String status;
Status(String status) {
this.status = status;
}
}
@Converter
public class StatusConverter implements AttributeConverter<Status, String> {
@Override
public String convertToDatabaseColumn(Status attribute) {
return attribute.getStatus();
}
@Override
public Status convertToEntityAttribute(String dbData) {
return Status.valueOf(dbData);
}
}

What I we achieve? I eliminated the transformations in the data layer and moved them to our domain. I got rid of the mapper class and we delegated it to JPA. Less lines of code with the same benefit.

Another compromise that we need to think of is pagination and filtering. Data is not part of the domain. We feed data into the domain and extract it after processing. The domain should not care or know details about the data. So what do we do when we need to paginate or filter data for presentation purposes. What are our choices here? Put pagination logic into the domain repositories and services? In that case our domain services and repositories will have to contain a value object that represents pagination.

@Value
public class PageVO {
private Integer pageNumber;
private Integer limit;
private Integer total;
}
public interface OrderRepository {
Order save(Order order);
Order get(Long orderId);
List<Order> gerOrders(PageVO pageVO);
}

This is the thing I always use as a compromise. Another thing what we can do is to separate this into a repository that is not a domain repository, that means it’s living in the infrastructure layer.

public class Page {
private Integer pageNumber;
private Integer limit;
private Integer total;
}
public interface OrderDtoRepository {
OrderDto save(OrderDto order);
OrderDto get(Long orderId);
List<OrderDto> gerOrders(Page page);
}

This can be regarded as a view. Which brings us the Command Query Responsibility Segregation(CQRS). It’s worth mentioning that the above example is not CQRS. Event though there are two representations of the same data (Order and OrderDto) they do not belong to the same layer. Order is part of the domain, while OrderDto is part of the infrastructure. Let’s return to CQRS. It a nutshell it means separating the writes(commands) from the reads(queries). In order to achieve this we would need to simplify our repositories and remove most of the reads. Then we would need to create specific view models.

We can use a simplified version of CQRS where both the commands and the queries use the same database. We win ACID but possibly lose on performance if some views are heavy. We don’t need domain events to be sent from the command to the query since the next time we would read this query we would get the latest data. Remember this a a simplified CQRS. If we go with full blown CRQS we need domain events and a two storages from commands and queries. If the situation needs it we can go further to Event Sourcing but at this point the complexity will increase and this decision should not be taken lightly.

@Value
public class PaginatedOrder {
private Long id;
private Integer amount;
private Integer productCode;
private Status status;
}
public interface PaginatedOrdersView {
List<PaginatedOrder> getOrders(Integer pageNumber, Integer limit);
}

In this example PaginatedOrder represents the query model and the interface is a way to fill it. Also it’s important for the query model to be immutable.

Another compromise that we need to think of is the workflow(BPMN). There are many workflow engines out there like flowable, camunda, etc. Remember that we said that the business is part of the domain. Actually is the core of the domain. What do we do in situations where the business use cases become complex and consists of multiple steps? Even more what if it contains some actions that are automatically and others which required human input? How can we design such a domain and it the same time leverage the power of BPMN engines? Well we can follow the same way as we did with JPA since there is a standard here as well BPMN 2.0 but unfortunately in the case of flowable(that’s what I tried) the features that we need are not part of the standard and there are pretty vital. This will not work so what I suggest here is to create a use case layer which uses only flowable specifics. Not ideal, but at least this should be easy to change without any impact on other layers. Keep it thin as it should be fine.

In conclusion DDD is not a silver bullet, but it’s a great way to describe your business models and like in life we need to make compromises. It the end our software should be good enough, not perfect.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK