Skip to content

Simple abstractions and aspects to communicate within one process following the CQS principle

Notifications You must be signed in to change notification settings

prisma-capacity/spring-cqs

Repository files navigation

Spring-CQS

Simple abstractions we use to follow the CQS Principle in applications.

Java CI Codacy Badge codecov MavenCentral

Motivation

The CQS Principle states that "every method should either be a command that performs an action, or a query that returns data to the caller, but not both." in order to reduce side-effects.

In our projects we use abstractions like Query & QueryHandler as well as Command & CommandHandler to follow this principle. However, there is a bit of fineprint here that makes it worthwhile to reuse this in form of a library:

  • a command / a query needs to be valid (as in java.validation valid), otherwise a Command/Query-ValidationExcption will be thrown
  • a command / a query needs to be valid (determined by an optional message on the handler), otherwise a Command/Query-ValidationExcption will be thrown
  • a command / a query needs to be verified by a mandatory method in the handler the is expected to throw a Command/Query-VerificationException
  • when a command / a query is handled, any exception it may throw is to be wrapped in a Command/Query-Handling Exception

In order to accomplish that, this kind of orchestration is done by an aspect, in order to get this out of that way when following the call stack in your IDE.

This has pros and cons and might be a debatable use of aspects, but we decided that this is the best solution for our context. You know, it depends...

Usage

This is meant to be used with Spring Boot. In order to get this running just add the dependency to your build system:

Maven

    <dependency>
      <groupId>eu.prismacapacity</groupId>
      <artifactId>spring-cqs</artifactId>
      <version><!-- put the desired version in here--></version>
    </dependency>

Spring Boot Compatibility

Library version Spring Boot version
2.x.x 2.7+
3.x.x 3.1+

Configuration

The only thing you might want to configure is how Cqs uses Metrics. See @CqsConfiguration for details.

Example

Let's say, you have a Foo Entity and a corresponding repository. What we do with this lib is to encapsulate use-cases in a UI-agnosic manner.

class FooEntity {
}

class FooQuery implements Query {
    @NotNull
    UUID idToLookFor;

    @NotNull
    Long userIdOfRequestingUser;
}

class FooHandler implements QueryHandler<FooQuery, List<FooEntity>> {

    @Override
    public void verify(@NonNull FooQuery query) throws QueryVerificationException {
        // check if the preconditions for the query to be executed are met.
        // we know userIdOfRequestingUser is not null (otherwise it would not have passed validation)
        // but maybe we need to check if the user is assigned to the right organisation or something...
    }

    @Override
    public List<FooEntity> handle(@NonNull FooQuery query) throws QueryHandlingException, QueryTimeoutException {
        return myFooRepository.findById(query.idToLookFor);
    }
}

The idea here is (beyond javax.validation), you can quickly see the ins and outs of a use-case, may it be Query or Command, including checking for instance security constraints in a programmatic and technology agnostic way. Also this creates a nice seam between UI/Rest Layer and Domain Model or persistence model in case this is the same for you. If you're interested in checking and maintaing those bounds, have a look at for instance Archunit.

Configure a retry behaviour for Command and Query handlers

If you want you can configure a retry behaviour for your handlers by adding @RetryConfiguration to the handler class. By default, it will retry 3 times in intervals of 20ms for every exception that is not of type QueryValidationException or CommandValidationException. You can also configure an exponential backoff if desired. Please have a look at RetryConfiguration.java for all available options.

Tips for use

  • Create one handler per use-case.
  • Do not call other handlers inside of yours. If that leads to code duplication, consider refactoring common code into a service that will be used by the two handlers.

Migration

1.0 -> 1.1:

In 1.0 CommandHandler returned a CommandTokenResponse. While this is useful in some cases, the majority of uses could go with a void return. For this reason, we have a breaking change in 1.1, where CommandHandler was renamed to TokenCommandHandler, and the new CommandHandler now return void.

So please make your CommandHandlers extend TokenCommandHandler as a minimal change. If you don't use the token, you may want to just return void instead.

About

Simple abstractions and aspects to communicate within one process following the CQS principle

Resources

Stars

Watchers

Forks

Packages

No packages published