Using Domain Driven Design (in short DDD) it is common to follow the principles of hexagonal, onion or ports and adapters architecture. This means that your domain logic should be independent from how it is connected to the outside world. For instance you shouldn’t see any messaging or REST specific logic in domain logic. Even though this sounds easy, in past projects I came across loads of messy code when connecting the outside world (like a REST Endpoint) with the domain. Doing it in a more functional way using functions solves a lot of problems and leads to cleaner code. I want to share this in this blog post.
The problem
Data always has to be mapped. There often exists something like the following:
- Especially if your are using java and JPA you have to differentiate between creating new objects or updating existing ones. This is necessary because in the same JPA context there should exist only one object with the same id.
- When your domain object has a list of other objects (e.g. your order may have a list of orderpositions), you don’t want to throw away the whole list and create a new one which may lead to lots of deletes and updates just because the customer wanted to increase the amount. Instead, only the relevant orderposition entry should be updated in the database and the others left unchanged.
- When your domain object has a list of other objects (e.g. your order may have a list of orderpositions), you don’t want to throw away the whole list and create a new one which may lead to lots of deletes and updates just because the customer wanted to increase the amount. Instead, only the relevant orderposition entry should be updated in the database and the others left unchanged.
So what’s the problem? The problem (at least in Java) is how to convert between your adapter model (REST resource or message) to your domain model. Having to read the domain object from the database in a class which converts the REST resource to your domain object is very cumbersome. Furthermore you may have duplicated code in other parts of your code, e.g. where messages are parsed and converted to your domain object. Last but not least I normally don’t want to care whether the objects exist or not when I write such converters. The domain logic should handle that.
Some messy attempts
In the past I saw the following approaches:
- Writing intermediate objects. Instead of mapping something like your REST Resource to your domain object, you map your REST object to an intermediate object and pass this to your business logic. The intermediate object can be created by different adapters. This leads to writing lots of mapper code and you are unable to use the fancy domain methods which you have written. Furthermore naming this intermediate objects is often hard which is a bad sign.
- You pass both, the domain object and the REST Resource, to your converter. This leads to an ugly / more complicated interface of your services.
- During the transformation, the converters lookup the domain objects in the database itself to handle the case of updating versus inserting them. This leads to the disadvantages mentioned before. In some cases the code is not too bad, but can become very ugly. At least this lookup code may be duplicated in different converters.
The better solution
The solution is simple, just pass functions. Your converters in the different adapters don’t create or update the domain object immediately. Instead they return a function which expects the domain object as input and updates it. This function is then passed to the domain logic which then applies it and hence updates the domain object. Such a converter may look like this:
// these lombok annotations if you don't know them are generating
// gettes and builder methods ...
@Builder
@Getter
public class CatalogArticleUpdate {
// the functions which update the domain object, may never be applied!
// You create an own interface which extends Function<> if you like
private final Function<Article, Article> articleUpdate;
// some other fields which the domain services needs to figure out to which
// domain object, the update needs to be applied
private final Catalogue catalogue;
private final ExternalDocId externalDocId;
}
public class CatalogueAricleChangedMessageConverter {
// this method does not convert the message immediately but instead returns
// a function which does this when it is applied
public CatalogArticleUpdate convert(final CatalogueAricleChangedMessage message) {
return CatalogArticleUpdate.builder()
.catalogue(Catalogue.valueOf(message.getCatalogueId()))
.externalDocId(ExternalDocId.valueOf(message.getDocId()))
.articleUpdate(article -> {
// if the domain service calls this method, it provides the domain
// object (see article parameter) so we don't need to lookup
// it ourselves :-)
article.updateTitle(ArticleTitle.valueOf(message.getTitle())):
article.updateCataloguePrices(convertePrices(message));
// much more updates ....
return article;
})
.build();
}
}
This approach has several advantages:
- More business logic can be put in the domain layer.
- The domain layer can even decide to ignore the update. In that case the update function is not executed. For instance, in my current project we get a lot of price and metadata updates from a catalogue which contains lots of articles. For our service, only a small amount of these updates are relevant. So the domain service may decide to ignore an update. Or it may decide to create a new one. Or it may decide to update several domain objects in the database because there exist different ones for different tenants. The point is, this logic how to apply the update belongs into the domain layer
- Code in the domain layer may be reused. The example above creates a CatalogArticleUpdate for a message. A batch job, for instance, may load updates via feed and converts the result from the feed to an instance of CatalogArticleUpdate and calling the same domain service.
- No unnecessary intermediate objects are needed so there is no need to write mapping code for them.
- Very flexible, all of your fancy domain methods can be used.
- Easy to test, just apply the function to something.
- The resulting code is not so cumbersome.
In my current project we have used this style to just return functions instead of doing the conversion in the converters immediately for some time and it turned out nicely.
Top comments (0)