Table of Contents generated with DocToc
DTO term used in this post refers only to the Data Transfer Object
, and not value objects, or any other objects carrying anything else than data initialized to avoid expensive lazy computation.
A Projection is a DTO.
We often encounter hard times refactoring and upgrading our libraries. Most of the time it goes smoothly and upgrading is straightforward: adding few flags, removing others, refactoring methods are always pretty easy and impactless tasks (with a well tested code base). But when it comes to changing paradigm, refactoring domains, changing entity relationships,... it is where things becomes messy:
- you will most likely duplicate code to satisfy your legacy clients
- you will have to create new transformers (domain to DTO) or change your DTOs
- you may have to change the whole stack, or create new endpoints using your new code
This issue is really important, for we often change technology, sometimes for huge improvement, sometimes for clarity purpose, sometimes to respond to new business needs,...
Now, it is important to note that right now, this is not an issue we fixed, but a simple draft on how to do it.
As a REST API developer, what are you if no one consumes your API?
You are bound to provide your customer with data first.
Does anyone but you even cares whether you are using Hibernate or jooq or plain JDBC, as long as they have their DTOs?
Do they even care that your table SHOE
as a join table to SIZE
called EXISTS_IN
?
We can rapidly notice that your DTO is the core of your REST API, way before your domain or business services.
The first suggestion of this post is then to create your DTO before your domain, and let it drive your design.
The shoe example
Maven configuration
<parent>
<artifactId>demo</artifactId>
<groupId>com.example</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>dto</artifactId>
An important part of the idea is also that your controllers will be de facto static once it will be consumed by at least one client. Indeed, you cannot control the workload of your clients, hence you will have to adapt to them, and guarantee them your service for as long as you judge fit (in practice, since money drives us, if the client is worth to keep, we all know the controller will remain untouched as long as this client does not migrate).
This means that you will need your controller to be agnostic (as for the DTOs) of the business implementation. Once again, do your customer care if you are using a new table which requires them to update to a new API, if they do not need it anyway?
So the whole point here is again to make your controller implementation agnostic of the core implementation.
The shoe example
Maven configuration
<parent>
<artifactId>demo</artifactId>
<groupId>com.example</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>controller</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>dto</artifactId>
<version>${parent.version}</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>core</artifactId> <!-- Explanations are coming -->
<version>${parent.version}</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>core-legacy</artifactId> <!-- Explanations are coming -->
<version>${parent.version}</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>core-new</artifactId> <!-- Explanations are coming -->
<version>${parent.version}</version>
</dependency>
</dependencies>
ShoeController
@Controller
@RequestMapping(path = "/shoes")
@RequiredArgsConstructor
public class ShoeController {
private final ShoeFacade shoeFacade;
@GetMapping(path = "/search")
public ResponseEntity<Shoes> all(ShoeFilter filter, @RequestHeader BigInteger version){
return ResponseEntity.ok(shoeFacade.get(version).search(filter));
}
}
Well, we all know your controller needs to call some business code, whether to access direct domain data, or to compute some complex operations. So the whole point of making your controller "business-agnostic" is to create a core abstraction, depending only on DTOs. The contracts of this implementation should be something like:
Given the input DTO, return an output DTO.
As simple as that. This way, your core implementation indeed depends only on DTOs.
The shoe example
Maven configuration
<parent>
<artifactId>demo</artifactId>
<groupId>com.example</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>core</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>dto</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
ShoeFacade
@Component
public class ShoeFacade {
private Map<BigInteger, ShoeCore> implementations = new HashMap<>();
public ShoeCore get(BigInteger version){
return implementations.get(version);
}
public void register(BigInteger version, ShoeCore implementation){
this.implementations.put(version, implementation);
}
}
ShoeCore
public interface ShoeCore {
Shoes search(ShoeFilter filter);
}
AbstractShoeCore
public abstract class AbstractShoeCore implements ShoeCore {
@Autowired
private ShoeFacade shoeFacade;
@PostConstruct
void init(){
val version = Optional.ofNullable(this.getClass().getAnnotation(Implementation.class))
.map(Implementation::version)
.orElseThrow(() -> new FatalBeanException("AbstractShoeCore implementation should be annotated with @Implementation"));
shoeFacade.register(version, this);
}
}
Implementation
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface Implementation {
int version();
}
Okay, so we have an abstract core, how do we register cores implementations so that your built application can use them?
Once again, it is quite simple: create a factory in the core abstraction, and let your core implementation register against this factory.
Once you do that, your business implementation will be picked by the factory, instead of the controllers directly. Controllers will only need to integrate the factory.
The shoe example
Maven configuration
<parent>
<artifactId>demo</artifactId>
<groupId>com.example</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>core-new</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>core</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
Following a REST API versioning guide, we think versioning using content negotiation is pretty relevant for our purpose:
- we will be able to return different DTO for the same endpoint
- we may simply fetch the
version
from the content type, and fetch our core implementation accordingly - our controllers will not depend on core implementation, but on the factory providing us core implementation
To run the application, you can run the following command in the root folder of the project:
mvn clean package && \
java -jar controller/target/controller-1.0.jar
To test version 1, you can call:
curl -X GET "http://localhost:8080/shoes/search" -H "version: 1"
which should answer (see com.example.demo.core.ShoeCoreLegacy.search
):
{"shoes":[{"name":"Legacy shoe","size":1,"color":"BLUE"}]}
To test version 2, you can call:
curl -X GET "http://localhost:8080/shoes/search" -H "version: 2"
which should answer (see com.example.demo.core.ShoeCoreNew.search
):
{"shoes":[{"name":"New shoe","size":2,"color":"BLACK"}]}
We can see that both result are structurally identical, while the code is obviously different.
This is indeed useful, since we can use almost any paradigm, segregate our code versions and eventually just drop one when implementation becomes unused and/or deprecated.