DEV Community

Eduards Sizovs
Eduards Sizovs

Posted on

How to design a service layer in Java using CRF

Intro

As part of my Effective Java course, I've been showing how to design a simple, but flexible service layer for Java applications with the approch I call CRF. Most attendees find the idea fresh and exciting, so I have decided to share it with the wider audience.

Service layer?!

Let's briefly discuss what service layer is supposed to do. The purpose of the service layer is to:

1) Define a use case. In other words, when you look at the service layer, it should answer the question – what the system is capable of doing.

2) Define a contract. In other words, when you look at the particular use case / service, it should be clear what data inputs are required, what are the possible outputs, and what exceptions are thrown.

3) Define a transaction boundary. A use case either succeeds or fails. If it succeeds, the associated data is persisted. If it fails – the changes are rolled back. This is important, because different users can run many use cases in parallel. Say, if me (Eduards) and you (the reader) are trying to secure the same airplane seat, there are 2 instances of a use case (SecureASeat) running in parallel. In order to avoid double booking (hello United Airlines), if you secure the seat first, my booking attempt must fail and all changes associated with my booking attempt (if any) must be rolled back.

There is much more to know about the service layer, but it's enough to set the common ground.

Now, let's look at the most common way of building a service layer.

Procedural service layer (Babushka style)

The most common style of a service layer is to use procedures.

Here is a service that allows messing with member permissions:

interface MemberPermissionService {
  void grantPermission(String memberId, String permissionId);
  void revokePermission(String memberId, String permissionId);
  PermissionsDto listPermissions(String memberId);
  ...
}
Enter fullscreen mode Exit fullscreen mode

Because we program to an interface, there is also the implementation:

@Service
class MemberPermissionServiceImpl implements MemberPermissionService {
  @Override
  public void grantPermission(String memberId, String permissionId) { ... }

  @Override
  void revokePermission(String memberId, String permissionId) { ... }

  @Override
  public PermissionsDto listPermissions(String memberId) { ... }
}
Enter fullscreen mode Exit fullscreen mode

Sometimes the interface is not used at all. Whether or no, it doesn't change the idea.

The aforementioned style is valid, but, as every design decision, it also has a set of drawbacks.

Opaqueness

Imagine we are looking at the package where services are located:

net.blog.sizovs.services.MemberPermissionService
net.blog.sizovs.services.ModerationService
net.blog.sizovs.services.MemberService
Enter fullscreen mode Exit fullscreen mode

Looking into the package, we can't get a solid understanding what the system is doing. We see that the system can do something with member permissions, something with moderation and something with members.

In order to understand what exactly the system can do, we have to open the services and see what procedures they contain. In other words, the service layer does not scream about what the system is capable of doing.

Obesity

All use cases related to Member Permissions become methods of MemberPermissionService interface. The more uses cases – the more methods.

The more methods an interface publishes, the higher is afferent coupling.

The more an interface promises to do, the fatter will be the implementation (MemberPermissionServiceImpl).

Very likely, MemberPermissionServiceImpl will also suffer from high efferent coupling – it will have many outgoing dependencies.

Large number of dependencies reduces testability.

It's like a snowball rolling down the hill. People just keep adding use cases to the same interface, because the interface has become a container for suitable uses cases. Sounds familiar?

Always blocking

In our example, the service calls are blocking. Technically, we can make some services return CompletableFuture or Observable.

Likely, we'll end up with a separate method per execution model. Like this:

interface MemberPermissionService {
  PermissionsDto listPermissions(String memberId);
  CompletableFuture<PermissionsDto> listPermissionAsync(String memberId);
  ...
}
Enter fullscreen mode Exit fullscreen mode

Some teams prefer creating a separate interface:

interface MemberPermissionServiceAsync {
  CompletableFuture<PermissionsDto> listPermissionAsync(String memberId);
  ...
}
Enter fullscreen mode Exit fullscreen mode

It's more a matter of taste, rather than a conceptual difference.

Remember the rule – encapsulate what varies? The service is essentially the same, but the execution model varies. Can we do a better job? Surely we can. Read on.

Side-note: what if we make all services return CompletableFuture from day 1 to support both blocking and non-blocking execution? Yes, we can! Nevertheless, there are other execution models such as "fire-and-forget", when you push the work into a message queue and the work is being run in the background. CompletableFuture won't help here.

No single entry point

Since there is no single entry point for services, it becomes challenging to log all service invocations, throttle them, implement unified transaction management strategy or a single point of validation.

The traditional solution to the problem is to create a bunch of annotations and instrument the code via AOP:

interface MemberPermissionService {
  @WrapInTransaction
  @Log
  @Validate
  @Throttle(max = 100)
  void grantPermission(String memberId, String permissionId);
  ...
}
Enter fullscreen mode Exit fullscreen mode

The solution serves the purpose, but has significant limitations, though:

  • What if we want to throttle invocations based on a dynamic parameter, instead of the hard-coded value of 100? With annotations you can't do this.
  • How can we ensure that @Throttle annotation processor runs before @WrapInTransaction to avoid creating unnecessary transactions? Very likely, we'll have to introduce invisible dependency between @WrapInTransaction and @Throttle processors. Not good.

There must be a better way. Read on.

Meet Commands / Reactions / Funnels (CRF)

CRF is a command-oriented approach to service layer design, comprising of 3 main components – Commands, Reactions and Funnels.

A command

A command defines a single use case, an action that the user can perform. For example – GrantPermission, RevokePermission and ListPermissions will be our commands.

Technically, commands implement a marker interface. Since commands can return the Response, R generic defines a response type for the command:

interface Command<T extends Command.R> {
  interface R {
    class Void implements R {
    }
  }
Enter fullscreen mode Exit fullscreen mode

A reaction

A reaction defines how the system reacts to a specific command. A reaction is where we write service code. For every command there must be a reaction. GrantPermissionReaction, RevokePermissionReaction and ListPermissionsReaction are our reactions.

Technically, reactions derive from a single interface. The interface is generic and defines command type and the response type.

We also add a default method commandType() that resolves the matching command type via generics. TypeToken comes from Guava library:

interface Reaction<C extends Command<R>, R extends Command.R> {
  R react(C $);

  default TypeToken<C> commandType() {
    return new TypeToken<C>(getClass()) {};
  }    
}
Enter fullscreen mode Exit fullscreen mode

A funnel

All commands pass through a funnel. The funnel defines how the command will be executed – blocking, non-blocking or fire-or-forget. Naturally, it also contains code that runs for all commands (e.g. logging).

The simplest systems have a single, synchronous funnel.

Below are interface examples of different funnels. Implementations will follow later:

Synchronous funnel

interface Now {
  public <R extends Command.R, C extends Command<R>> R execute(C command);
}
Enter fullscreen mode Exit fullscreen mode

Asynchronous funnel

interface Future {
  public <R extends Command.R, C extends Command<R>> CompletableFuture<R> schedule(C command);
}
Enter fullscreen mode Exit fullscreen mode

Reactive funnel

interface Rx {
  public <R extends Command.R, C extends Command<R>>  Observable<R> observe(C command);
}
Enter fullscreen mode Exit fullscreen mode

Durable fire-and-forget funnel

interface Durable {
  <C extends Command> void enqueue(C command);
}
Enter fullscreen mode Exit fullscreen mode

Putting it all together

Let's start with a GrantPermission command that returns no result:

class GrantPermission implements Command<Command.R.Void> {

  private final String memberId, permissionId;

  public GrantPermission(String memberId, String permissionId) { ... }

  public String memberId() {
    return memberId;
  }

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

Then we implement a reaction. Members and Permission are parts of our domain model. The reaction is a Spring-managed component, so Permissions and Members dependencies are injected.

@Component
class GrantPermissionReaction implements Reaction<GrantPermission, Command.R.Void> {

  private final Members members;
  private final Permissions permissions;

  public GrantPermissionReaction(Permissions permissions, Members members) { 
    ... 
  }

  @Override
  public Command.R.Void react(GrantPermissionCommand $) {
    Member member = members.byId($.memberId());
    Permission permission = permissions.byId($.permissionId());
    member.grantPermission(permission);
    return new Command.R.Void();
  }
}
Enter fullscreen mode Exit fullscreen mode

We also have to implement a synchronous funnel. When a command is passed through the funnel (called Now), the funnel routes the command to the appropriate reaction. The reactions are resolved from Spring application context:

interface Now {
  public <R extends Command.R, C extends Command<R>> R execute(C command);
}

@Component
class SimpleNow implements Now {

  private final ListableBeanFactory beanFactory;

  public Now(ListableBeanFactory beanFactory) {
    this.beanFactory = beanFactory;
  }

  public <R extends Command.R, C extends Command<R>> R execute(C command) {
    Class<? extends Command> commandType = command.getClass();
    Reaction<C, R> reaction = reactions()
                .stream()
                .filter(r -> r.commandType().isSupertypeOf(commandType))
                .findFirst()
                .orElseThrow(() -> new IllegalStateException("No reaction found for " + commandType));

    return reaction.react(command);
  }

  private Collection<Reaction> reactions() {
    return beanFactory.getBeansOfType(Reaction.class).values();
  }
}
Enter fullscreen mode Exit fullscreen mode

The aforementioned example, however, asks Spring to provide all reactions every time a command passes through the funnel. This can hit the performance. Let's add Caffeine caching to make sure that the funnel remembers which reaction corresponds to a command –

@Component
class SimpleNow implements Now {

  private final LoadingCache<Type, Reaction> reactions;

  public Now(ListableBeanFactory beanFactory) {
    this.reactions = Caffeine.newBuilder()
            .build(commandType -> reactions(beanFactory)
             .stream()
             .filter(reaction -> reaction.commandType().isSupertypeOf(commandType))
             .findFirst()
             .orElseThrow(() -> new IllegalStateException("No reaction found for " + commandType);
  }

  public <R extends Command.R, C extends Command<R>> R execute(C command) {
    Reaction reaction = reactions.get(command.getClass());
    return (R) reaction.react(command);
  }

  private Collection<Reaction> reactions(ListableBeanFactory beanFactory) {
    return beanFactory.getBeansOfType(Reaction.class).values();
  }

}
Enter fullscreen mode Exit fullscreen mode

Bingo! As we have glued all the components together, now we can look at the client code:

class Application {

  @Autowired
  Now now;

  public void run(String memberId, String permissionId) {
    GrantPermission grantPermission = new GrantPermission(memberId, permissionId);
    now.execute(grantPermission);
  }
}
Enter fullscreen mode Exit fullscreen mode

...if asynchronous funnel is used, it will look like this –

class Application {

  @Autowired
  Future future;

  public void run(String memberId, String permissionId) {
    GrantPermission grantPermission = new GrantPermission(memberId, permissionId);
    future.schedule(grantPermission)
      .thenAccept(System.out::println)
      .thenAccept(...);
  }
}
Enter fullscreen mode Exit fullscreen mode

...if RxJava funnel is used, it will look like this –

class Application {

  @Autowired
  Rx rx;

  public void run(String memberId, String permissionId) {
    GrantPermission grantPermission = new GrantPermission(memberId, permissionId);   
    rx.observe(grantPermission).subscribe(System.out::println);
  }
}
Enter fullscreen mode Exit fullscreen mode

Amazing, isn't it?, The client can choose an appropriate funnel, depending on its requirements.

Command on steroids

Let's add a couple of default methods to Command interface, in order to make the client code sexier:

interface Command<T extends Command.R> {

  default T execute(Now now) {
    return now.execute(this);
  }

  default CompletableFuture<T> schedule(Future future) {
    return future.schedule(this);
  }

  default Observable<T> observe(Rx rx) {
    return rx.observe(this);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now you can invoke commands in a natural way:

grantPermission.execute(now);

grantPermission.schedule(future);

grantPermission.observe(rx);
Enter fullscreen mode Exit fullscreen mode

Command composition

Commands are great because they can be composed (decorated). To demonstrate the idea, let's implement retry mechanism. If the commands fails with an exception, the system will retry it several times before bubbling up the exception.

We create a special Try command that can wrap any command:

class Try<C extends Command<R>, R extends Command.R> implements Command<R> {

  private final C origin;
  private long times = 3;

  public Try(C origin) {
    this.origin = origin;
  }

  public Command<R> origin() {
    return origin;
  }

  public Try<C, R> times(long times) {
    this.times = times;
    return this;
  }

  public long times() {
    return times;
  }
}
Enter fullscreen mode Exit fullscreen mode

Then we create a reaction. To implement retry, we use SimpleRetryPolicy class, that is part of Sprint Retry project.

class TryReaction<C extends Command<R>, R extends Command.R>
                                          implements Reaction<Try<C, R>, R> {
  private final Router router;

  @Override
  public R react(Try<R> $) {
    SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
    retryPolicy.setMaxAttempts($.times());
    RetryTemplate template = new RetryTemplate();
    template.setRetryPolicy(retryPolicy);
        return template.execute(context -> {
            Command<R> origin = $.origin();
            Reaction<Command<R>, R> reaction = router.route(origin);
            return reaction.react(origin);
        });
  }
}
Enter fullscreen mode Exit fullscreen mode

TryReaction depends on a new class called Router – a class that encapsulates command routing logic to reactions, that was previously part of SimpleNow funnel:

@Component
class Router {

  private final LoadingCache<Type, Reaction> reactions;

  public Rounter(ListableBeanFactory beanFactory) {
    reactions = Caffeine.newBuilder()
                  .build(commandType -> reactions(beanFactory)
                      .stream()
                      .filter(reaction -> reaction.commandType().isSupertypeOf(commandType))
                      .findFirst()
                      .orElseThrow(() -> new IllegalStateException("No reaction found for " + commandType)));
    }

  private Collection<Reaction> reactions(ListableBeanFactory beanFactory) {
    return beanFactory.getBeansOfType(Reaction.class).values();
  }

  public <C extends Command<R>, R extends Command.R> Reaction<C, R> route(C command) {
    return reactions.get(command.getClass());
  }
}
Enter fullscreen mode Exit fullscreen mode

Here is how the client code will look like:

new Try<>(
  new GrantPermission(...))
      .times(5)
      .execute(now);
Enter fullscreen mode Exit fullscreen mode

I know you are amazed. I am too.

Funnels on steroids

Let's enrich our funnel so all commands are logged and executed in a transaction.

Let's change our SimpleNow funnel. We will call it PipedNow in honour of *nix pipes:

interface Now {
  public <R extends Command.R, C extends Command<R>> R execute(C command);
}

@Component
class PipedNow implements Now {
  public <R extends Command.R, C extends Command<R>> R execute(C command) {
    Now pipe = new Loggable(
                     new Transactional(
                            new Reacting()));
    return pipe.execute(command);
  }
}
Enter fullscreen mode Exit fullscreen mode

Loggable, Transactional are just plain old decorators. No magic:

class Loggable implements Now {

  private final Logger log = LoggerFactory.getLogger(Loggable.class);

  private final Now origin;

  public Loggable(Now origin) {
    this.origin = origin;
  }

  @Override
  public <R extends Command.R, C extends Command<R>> R execute(C command) {
    log.info(">>> {}", command.toString());
    R response = origin.execute(command);
    log.info("<<< {}", response.toString());
    return response;
  }
}
Enter fullscreen mode Exit fullscreen mode
class Transactional implements Now {

  private final Now origin;
  private final TransactionTemplate tx;

  public Transactional(Now origin) {
    this.origin = origin;
    this.tx = new TransactionTemplate(txManager);
  }

  @Override
  public <R extends Command.R, C extends Command<R>> R execute(C command) {
    return tx.execute(txStatus -> origin.execute(command));
  }
}
Enter fullscreen mode Exit fullscreen mode
class Reacting implements Now {

  @Override
  public <R extends Command.R, C extends Command<R>> R execute(C command) {
    Reaction<C, R> reaction = router.route(command);
    return reaction.react(command);
  }
}
Enter fullscreen mode Exit fullscreen mode

It's very easy to add new decorators. The best part of it is that decorators' execution order is explicitly visible in code. No magic.

OK, what benefits did we get?

  • Every use case is written in a separate class – less dependencies, testability is improved.
  • Every use case passes through a single entry point – a funnel. This is very convenient for centralised validation, logging, transactional management, throttling, providing the correlation identifier. No AOP magic.
  • Client can choose a command execution model by switching a funnel – sync, async, reactive, scheduled. You can even create a mock funnel for testing.
  • Service layer screams what the application is capable of doing:
net.blog.sizovs.services.GrantPermission

net.blog.sizovs.services.RevokePermission

net.blog.sizovs.services.ListPermissions
Enter fullscreen mode Exit fullscreen mode
  • You can add new parameters to the existing commands and response types w/o changing service signatures.
  • We got flexibility at command, reaction and funnel sides almost for free. As the system evolves, we can add more functionality to our service layer w/o large changes.

Final words

CRF is a nice alternative to procedural service layer. I use it in most of the enterprise apps I build and it has proven to be extremely effective.

There is much more you can do with CRF. For example, command responses can be cached by holding results against a key synthesized from the command name and arguments. Or, you can have multiple versions of the same reaction and choose which one to execute depending on a context. Amazing!

The working code example can be found on my GitHub. If you are interested in learning more about this approach – join my Effective Java class.

Top comments (0)