DEV Community

Davi Bandeira
Davi Bandeira

Posted on

From Services to Command and Handlers: use case driven code

Recently a colleague of mine showed me how MediatR works - a .NET implementation of the mediator design pattern which, in short, "define an object that encapsulates how a set of objects interact"¹. 

The idea sounded interesting considering that, unless a framework requires some specific way to set up the files and their responsibilities, my default approach for most projects is to use the 3-Tier Architecture or layered architecture

Don't get me wrong: I still think that having a presentation, business and persistence layer (or some variant of this) still works for most cases and should be the way to go². However, I had a personal project (written in Java with SpringBoot and Spring Data JPA) that had grown in size and complexity and wanted to try something 'new' while refactoring.

The first thing was to find a Java equivalent of MediatR. While looking for alternatives I came across PipelinR. The source code was more or less what I expected for the proposed functionality so it was time to start playing with it.


Before we get into refactoring part, I'd like to show what the project originally looked like:

Package structure (inspired by the BCE pattern)

├── boundary
│   └── ColorsResource.java         
├── control
│   ├── ColorRepository.java
│   └── ColorService.java
└── entity
    └── Color.java
Enter fullscreen mode Exit fullscreen mode

ColorResource.java:

@RestController
@RequestMapping("/colors")
public class ColorsResource {

  private final ColorService service;

  public ColorsResource(final ColorService service) {
    this.service = service;
  }

  @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<Color> insert(@Valid @RequestBody Color color) {
    var data = service.insert(color);
    var uri = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
        .buildAndExpand(data.getId()).toUri();
    return ResponseEntity.created(uri).body(data);

   // (other endpoints omitted)
  }
Enter fullscreen mode Exit fullscreen mode

ColorService.java

@Service
public class ColorsService {

  private final ColorRepository repository;

  public ColorsService(final ColorRepository repository) {
    this.repository = repository;
  }

  public Color insert(Color color) {
    try {
      return repository.save(color);
    } catch (DataIntegrityViolationException e) {
      throw new ResponseStatusException(
          HttpStatus.CONFLICT,
          MessageFormat.format("A Color with name [{0}] already exists.", color.getName()));
    }

   // (other methods omitted)
  }
Enter fullscreen mode Exit fullscreen mode

ColorRepository.java

@Repository
public interface ColorRepository extends JpaRepository<Color, Integer> {
}
Enter fullscreen mode Exit fullscreen mode

Color.java

@Entity
@Table(name = "colors")
public class Color {

  private static final long serialVersionUID = 4L;

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Integer id;

  @NotBlank(message = "Color.name cannot be blank.")
  @Column(unique = true, nullable = false)
  private String name;

  @Column(name = "created_date", nullable = false, updatable = false)
  private OffsetDateTime createdDate;

  @Column(name = "updated_date")
  private OffsetDateTime updatedDate;

  // (constructor, getters and setters omitted)
}
Enter fullscreen mode Exit fullscreen mode

Pipeline, Commands and Handlers

The first step to start using Pipelinr is to add the Maven dependency and the repository where it's hosted:

<dependency>
  <groupId>an.awesome</groupId>
  <artifactId>pipelinr</artifactId>
  <version>0.5</version>
</dependency>

<repositories>
  <repository>
    <id>central</id>
    <name>bintray</name>
    <url>https://jcenter.bintray.com</url>
  </repository>
</repositories>
Enter fullscreen mode Exit fullscreen mode

The second step is to create a managed instance of a Pipeline to be injected in our classes:

@Configuration
public class PipelinrProvider {

  @Bean
  public Pipeline getPipeline(
      ObjectProvider<Handler> commandHandlers,
      ObjectProvider<Notification.Handler> notificationHandlers,
      ObjectProvider<Command.Middleware> middlewares) {
    return new Pipelinr()
        .with(commandHandlers::stream) // Registers Handlers
        .with(notificationHandlers::stream) // Registers Notifications (not covered here)
        .with(middlewares::orderedStream); // Registers Middlewares (not covered here)
  }
}
Enter fullscreen mode Exit fullscreen mode

With this in place we can now start refactoring.

Commands

Roughly speaking, commands are objects that encapsulate the details of a request and capture its intent³. In our case, we expose a POST endpoint with the intent of creating a new color. So let's create two commands: one for capturing the color creation intent and one with the outcome of the intent (a created color).

ColorResponseCommand.java

public final class ColorResponseCommand {

  private final int id;
  private final String name;
  private final OffsetDateTime createdDate;

  private ColorResponseCommand(final int id, 
       final String name, 
       final OffsetDateTime createdDate) {
    this.id = id;
    this.name = name;
    this.createdDate = createdDate;
  }

  public static ColorResponseCommand from(final Color color) {
    return new ColorResponseCommand(color.getId(), color.getName(), color.getCreatedDate());
  }

  // getters omitted
}
Enter fullscreen mode Exit fullscreen mode

CreateColorRequestCommand.java

public class CreateColorRequestCommand implements Command<ColorResponseCommand> {

  // Only name is required when creating a Color
  @NotBlank(message = "Name must not be blank.")
  private final String name;

  public CreateColorRequestCommand(final String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }
}
Enter fullscreen mode Exit fullscreen mode

Do note that the class above implements a Command interface from Pipelinr, defining the response as its parameterized type.

Handlers

Handlers in Pipelinr are classes that know how what to do with commands. It's very much similar to how Chain of Responsibility pattern works⁴. Since our use case is to create a color, let's add a handler for that:

@Component
public class CreateColorHandler implements
    Command.Handler<CreateColorRequestCommand, ColorResponseCommand> {

  private final ColorRepository repository;

  public CreateColorHandler(final ColorRepository repository) {
    this.repository = repository;
  }

  @Override
  public ColorResponseCommand handle(final CreateColorRequestCommand command) {
    var color = new Color();
    color.setName(command.getName());
    try {
      var createdColor = repository.save(color);
      return ColorResponseCommand.from(createdColor);
    } catch (DataIntegrityViolationException e) {
      throw new ResponseStatusException(
          HttpStatus.CONFLICT,
          MessageFormat
              .format("A Color with name [{0}] already exists.", command.getName()));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

There are a few things to notice:

  1. Our managed class implements Command.Handler, which defines that this handler will receive a CreateColorRequestCommand and respond with a ColorResponseCommand.
  2. By implementing the interface mentioned above and making the instance managed (@Component annotation), this handler will be registered and available for use by the Pipeline instance (more on this on the next topic).

Pipeline

It's now time to put the handler and commands in action. As seen previously, our ColorsResource.java delegated the creation of a Color to the injected ColorService.java. Instead, let's use the Pipeline instance we defined earlier. It works as a mediator between commands and handlers as it knows to which handler to dispatch each command in the application.

@RestController
@RequestMapping("/colors")
public class ColorsResource {

  private final Pipeline pipeline;

  public ColorsResource(final Pipeline pipeline) {
    this.pipeline = pipeline;
  }

  @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<ColorResponseCommand> insert(@Valid @RequestBody CreateColorRequestCommand command) {
    var data = pipeline.send(command);
    var uri = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
        .buildAndExpand(data.getId()).toUri();
    return ResponseEntity.created(uri).body(data);
  }
}
Enter fullscreen mode Exit fullscreen mode

With this new setup, we can now get rid of our ColorService.java - which in a real world would have several methods - and instead create handlers and commands for each use case.

Let's see some of the pros and cons of adopting such solution.

Pros

  • Decouples the invoker and receiver of a command by relying on a mediator to dispatch it to the right handler.
  • Unit testing is simpler by having only the bare minimum to satisfy the dependencies of handlers and commands.
  • The file names reflect the application use cases, making navigation clearer (debatable).
  • Each command exposes only the attributes needed for a particular use case, avoiding sending and receiving fields that should not be applicable/exposed in all use cases.
  • Reduces the possibility of merge conflicts in big teams, since use cases are decentralized from a single Service file.
  • Simplifies adding new features/use cases to the application. As new handlers and commands are likely to be needed, existing code might not be affected.

Cons

  • The IDE won't be your friend when trying to navigate to correct handler via the pipeline.send() method.
  • Each new use case requires a new handler and at least one new command to be created, resulting in a lot of classes the project.
  • Depending on the implementation of the mediator/pipeline object, there could be some performance impact.

Conclusion

Let's be honest: it's a lot of code for something as simple as CRUD operations. But this approach shines in certain scenarios, listed in the Pros section above.

While the examples shown here relied on Pipelinr, there are other alternatives out there (such as https://github.com/kmhigashioka/ShortBus) that embrace the same concepts.

The final code can be found at https://github.com/davibandeira/pipelinr-demo.

Let me know your thoughts about this and other approaches ;)


¹ Gamma, Erich- Design Patterns: Elements of Reusable Object-Oriented Software: Addison-Wesley, 1994.
² YAGNI and KISS principles.
³ https://refactoring.guru/design-patterns/command
https://sourcemaking.com/design_patterns/chain_of_responsibility

Top comments (2)

Collapse
 
emmanuelkaranja profile image
emmanuel-karanja • Edited

Hi Davi,

I was looking for this exact thing, I've used MediatR in several of my projects and I loved it. Thank you very much! Keep up the good work.

Collapse
 
davibandeira profile image
Davi Bandeira

Hi Emmanuel! Thank you for the kind words. I'm glad this was helpful to you. Take care!