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);
...
}
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) { ... }
}
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
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);
...
}
Some teams prefer creating a separate interface:
interface MemberPermissionServiceAsync {
CompletableFuture<PermissionsDto> listPermissionAsync(String memberId);
...
}
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);
...
}
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 {
}
}
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()) {};
}
}
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);
}
Asynchronous funnel
interface Future {
public <R extends Command.R, C extends Command<R>> CompletableFuture<R> schedule(C command);
}
Reactive funnel
interface Rx {
public <R extends Command.R, C extends Command<R>> Observable<R> observe(C command);
}
Durable fire-and-forget funnel
interface Durable {
<C extends Command> void enqueue(C command);
}
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;
}
}
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();
}
}
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();
}
}
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();
}
}
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);
}
}
...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(...);
}
}
...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);
}
}
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);
}
}
Now you can invoke commands in a natural way:
grantPermission.execute(now);
grantPermission.schedule(future);
grantPermission.observe(rx);
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;
}
}
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);
});
}
}
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());
}
}
Here is how the client code will look like:
new Try<>(
new GrantPermission(...))
.times(5)
.execute(now);
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);
}
}
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;
}
}
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));
}
}
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);
}
}
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
- 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)