1. Overview

In this quick article, we're going to do something new. We're going to evolve an existing REST Spring API and make it use Command Query Responsibility Segregation – CQRS.

The goal is to clearly separate both the service and the controller layers to deal with Reads – Queries and Writes – Commands coming into the system separately.

Keep in mind that this is just an early first step towards this kind of architecture, not “an arrival point”. That being said – I'm excited about this one.

Finally – the example API we're going to be using is publishing User resources and is part of our ongoing Reddit app case study to exemplify how this works – but of course, any API will do.

2. The Service Layer

We'll start simple – by just identifying the read and the write operations in our previous User service – and we'll split that into 2 separate services – UserQueryService and UserCommandService:

public interface IUserQueryService {

    List<User> getUsersList(int page, int size, String sortDir, String sort);

    String checkPasswordResetToken(long userId, String token);

    String checkConfirmRegistrationToken(String token);

    long countAllUsers();

}
public interface IUserCommandService {

    void registerNewUser(String username, String email, String password, String appUrl);

    void updateUserPassword(User user, String password, String oldPassword);

    void changeUserPassword(User user, String password);

    void resetPassword(String email, String appUrl);

    void createVerificationTokenForUser(User user, String token);

    void updateUser(User user);

}

From reading this API you can clearly see how the query service is doing all the reading and the command service isn't reading any data – all void returns.

3. The Controller Layer

Next up – the controller layer.

3.1. The Query Controller

Here is our UserQueryRestController:

@Controller
@RequestMapping(value = "/api/users")
public class UserQueryRestController {

    @Autowired
    private IUserQueryService userService;

    @Autowired
    private IScheduledPostQueryService scheduledPostService;

    @Autowired
    private ModelMapper modelMapper;

    @PreAuthorize("hasRole('USER_READ_PRIVILEGE')")
    @RequestMapping(method = RequestMethod.GET)
    @ResponseBody
    public List<UserQueryDto> getUsersList(...) {
        PagingInfo pagingInfo = new PagingInfo(page, size, userService.countAllUsers());
        response.addHeader("PAGING_INFO", pagingInfo.toString());
        
        List<User> users = userService.getUsersList(page, size, sortDir, sort);
        return users.stream().map(
          user -> convertUserEntityToDto(user)).collect(Collectors.toList());
    }

    private UserQueryDto convertUserEntityToDto(User user) {
        UserQueryDto dto = modelMapper.map(user, UserQueryDto.class);
        dto.setScheduledPostsCount(scheduledPostService.countScheduledPostsByUser(user));
        return dto;
    }
}

What's interesting here is that the query controller is only injecting query services.

What would be even more interesting is to cut off the access of this controller to the command services – by placing these in a separate module.

3.2. The Command Controller

Now, here's our command controller implementation:

@Controller
@RequestMapping(value = "/api/users")
public class UserCommandRestController {

    @Autowired
    private IUserCommandService userService;

    @Autowired
    private ModelMapper modelMapper;

    @RequestMapping(value = "/registration", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.OK)
    public void register(
      HttpServletRequest request, @RequestBody UserRegisterCommandDto userDto) {
        String appUrl = request.getRequestURL().toString().replace(request.getRequestURI(), "");
        
        userService.registerNewUser(
          userDto.getUsername(), userDto.getEmail(), userDto.getPassword(), appUrl);
    }

    @PreAuthorize("isAuthenticated()")
    @RequestMapping(value = "/password", method = RequestMethod.PUT)
    @ResponseStatus(HttpStatus.OK)
    public void updateUserPassword(@RequestBody UserUpdatePasswordCommandDto userDto) {
        userService.updateUserPassword(
          getCurrentUser(), userDto.getPassword(), userDto.getOldPassword());
    }

    @RequestMapping(value = "/passwordReset", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.OK)
    public void createAResetPassword(
      HttpServletRequest request, 
      @RequestBody UserTriggerResetPasswordCommandDto userDto) 
    {
        String appUrl = request.getRequestURL().toString().replace(request.getRequestURI(), "");
        userService.resetPassword(userDto.getEmail(), appUrl);
    }

    @RequestMapping(value = "/password", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.OK)
    public void changeUserPassword(@RequestBody UserchangePasswordCommandDto userDto) {
        userService.changeUserPassword(getCurrentUser(), userDto.getPassword());
    }

    @PreAuthorize("hasRole('USER_WRITE_PRIVILEGE')")
    @RequestMapping(value = "/{id}", method = RequestMethod.PUT)
    @ResponseStatus(HttpStatus.OK)
    public void updateUser(@RequestBody UserUpdateCommandDto userDto) {
        userService.updateUser(convertToEntity(userDto));
    }

    private User convertToEntity(UserUpdateCommandDto userDto) {
        return modelMapper.map(userDto, User.class);
    }
}

A few interesting things are happening here. First – notice how each of these API implementations is using a different command. This is mainly to give us a good base for further improving the design of the API and extracting different resources as they emerge.

Another reason is that when we take the next step, towards Event Sourcing – we have a clean set of commands that we're working with.

3.3. Separate Resource Representations

Let's now quickly go over the different representations of our User resource, after this separation into commands and queries:

public class UserQueryDto {
    private Long id;

    private String username;

    private boolean enabled;

    private Set<Role> roles;

    private long scheduledPostsCount;
}

Here are our Command DTOs:

  • UserRegisterCommandDto used to represent user registration data*:*
public class UserRegisterCommandDto {
    private String username;
    private String email;
    private String password;
}
  • UserUpdatePasswordCommandDto used to represent data to update current user password:
public class UserUpdatePasswordCommandDto {
    private String oldPassword;
    private String password;
}
  • UserTriggerResetPasswordCommandDto used to represent user's email to trigger reset password by sending an email with reset password token:
public class UserTriggerResetPasswordCommandDto {
    private String email;
}
  • UserChangePasswordCommandDto used to represent new user password – this command is called after user use password reset token.
public class UserChangePasswordCommandDto {
    private String password;
}
  • UserUpdateCommandDto used to represent new user's data after modifications:
public class UserUpdateCommandDto {
    private Long id;

    private boolean enabled;

    private Set<Role> roles;
}

4. Conclusion

In this tutorial, we laid the groundwork towards a clean CQRS implementation for a Spring REST API.

The next step will be to keep improving the API by identifying some separate responsibilities (and Resources) out into their own services so that we more closely align with a Resource-centric architecture.


« 上一篇: Baeldung周报第37期
» 下一篇: Java Web周报38