Domain Driven Design Compromises
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.
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.
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.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK