Skip to content

tbouyenval/core-abstract-sample

 
 

Repository files navigation

Table of Contents generated with DocToc

Explanations

Notes

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.

What are we talking about

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

How we address the issue

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.

Make DTOs the basis of your application

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>
File system

image

Make your controller agnostic of the core

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).

image

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>
File system

image

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));

  }

}

Core abstraction

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.

image

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>
File system

image

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();

}

Implementing your core

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>
File system

image

Versioning APIs

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

Validating the application

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

Version 1

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"}]}

Version 2

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"}]}

Conclusion

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.

About

Core abstraction example using Maven

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Java 100.0%