Semyon Kirekov
Posted on October 1, 2022
In this article, I'm showing you how to apply the Chain Of Responsibility pattern in your Spring application smoothly. You can find the source code in this GitHub repository.
Domain
Let's clarify the CoR pattern purpose. You can read its entire explanation by this link. Though now I'm sharing with you one particular example.
Imagine that we're creating a real-time enrichment service. It consumes a message, fulfils it with additional data, and produces the final enriched message as the output. Supposing we have 3 types of enrichment:
- Phone number determination by
SESSIONID
cookie. - The person's age retrieving by the
userId
field. - The person's geolocation obtaining by their IP address.
Development
Firstly, we need the EnrichmentStep
interface.
public interface EnrichmentStep {
Message enrich(Message msg);
}
The interface accepts the current Message
and returns the enriched one.
In this case, the
Message
class is immutable. Meaning thatEnrichmentStep
returns the new object but not the same one that is modified. Immutable classes usage is good practice because it eliminates lots of possible concurrency problems.
There are 3 types of enrichment. Therefore, we need 3 EnrichmentStep
implementations. I'm showing you the phone number example. Though if you're curious, you can see others in the repository.
@Service
public class PhoneNumberEnrichmentStep implements EnrichmentStep {
private final PhoneNumberRepository phoneNumberRepository;
@Override
public Message enrich(Message message) {
return message.getValue("SESSIONID")
.flatMap(phoneNumberRepository::findPhoneNumber)
.map(phoneNumber -> message.with("phoneNumber", phoneNumber))
.orElse(message);
}
}
In this case, we don't care about the
PhoneNumberRepository
implementation.
OK then. Each EnrichmentStep
might enhance the input message with additional data. But we need to proceed with all the enrichment steps to obtain the fulfilled message. The Chain of Responsibility pattern comes in handy. Let's rewrite the EnrichmentStep
interface a bit.
public interface EnrichmentStep {
Message enrich(Message message);
void setNext(EnrichmentStep step);
}
Each EnrichmentStep
implementation might reference the next chain element. Take a look at the modified PhoneNumberEnrichmentStep
implementation.
@Service
public class PhoneNumberEnrichmentStep implements EnrichmentStep {
private final PhoneNumberRepository phoneNumberRepository;
private EnrichmentStep next;
@Override
public Message enrich(Message message) {
return message.getValue("SESSIONID")
.flatMap(phoneNumberRepository::findPhoneNumber)
.map(phoneNumber -> next.enrich(
message.with("phoneNumber", phoneNumber)
))
.orElseGet(() -> next.enrich(message));
}
public void setNext(EnrichmentStep step) {
this.next = step;
}
}
Now the PhoneNumberEnrichmentStep
works a bit differently:
- If the message is successfully enriched, the fulfilled result proceeds to the next enrichment step.
- Otherwise the message with no modifications goes further.
It's time to connect EnrichmentStep
implementations into a linked list, i.e. build the Chain of Responsibility.
First of all, let's point out another essential detail. You see, the EnrichmentStep
defines that there is always the next step. But a chain cannot be infinite. Therefore, there is a possibility that the next chain element might be absent. In this case, we have to repeat not-null checks in every EnrichmentStep
. Because any implementation might be the last step. Thankfully there is a better alternative. The NoOpEnrichmentStep
is the implementation that just returns the same message without any actions. Take a look at the code block below.
public class NoOpEnrichmentStep implements EnrichmentStep {
@Override
public Message enrich(Message message) {
return message;
}
@Override
public void setNext(EnrichmentStep step) {
// no op
}
}
It allows us to set this object as the last chain element. So, it guarantees that the setNext
method is always invoked with some value and we don't have to repeat not-null checks.
The
NoOpEnrichmentStep
is actually an example of the Null Object Design pattern.
Now we're creating the EnrichmentStepFacade
. Take a look at the code snippet below.
@Service
public class EnrichmentStepFacade {
private final EnrichmentStep chainHead;
public EnrichmentStepFacade(List<EnrichmentStep> steps) {
if (steps.isEmpty()) {
chainHead = new NoOpEnrichmentStep();
} else {
for (int i = 0; i < steps.size(); i++) {
var current = steps.get(i);
var next = i < steps.size() - 1 ? steps.get(i + 1) : new NoOpEnrichmentStep();
current.setNext(next);
}
chainHead = steps.get(0);
}
}
public Message enrich(Message message) {
return chainHead.enrich(message);
}
}
The constructor accepts a list of all EnrichmentStep
implementations that are registered as Spring beans in the current application context (the framework does this automatically). If the list is empty, then the chainHead
is just the NoOpEnrichmentStep
instance. Otherwise, the current element is linked to the next one. But the last chain element always references NoOpEnrichmentStep
. Meaning that calling the first element of the provided list will execute the whole chain! What's even more exciting is that you can define the order of elements in the chain just by putting the Spring @Order
annotation. The injected List<EnrichmentStep>
collection will be sorted accordingly.
Refactoring
The generic chain element
Though the solution is working it's not complete yet. There are a few details to improve. Firstly, take a look at the EnrichmentStep
definition again.
public interface EnrichmentStep {
Message enrich(Message message);
void setNext(EnrichmentStep step);
}
The Chain of Responsibility is the generic pattern. Perhaps we'd apply it to another scenario. So, let's extract the setNext
method to the separate interface. Take a look at the definition below.
public interface ChainElement<T> {
void setNext(T step);
}
And now the EnrichmentStep
should extend it with the appropriate generic value.
public interface EnrichmentStep extends ChainElement<EnrichmentStep> {
Message enrich(Message message);
}
Chain building encapsulation
That's a slight improvement. What else can we do? Take a look at the EnrichmentStepFacade
definition down below again.
@Service
public class EnrichmentStepFacade {
private final EnrichmentStep chainHead;
public EnrichmentStepFacade(List<EnrichmentStep> steps) {
if (steps.isEmpty()) {
chainHead = new NoOpEnrichmentStep();
} else {
for (int i = 0; i < steps.size(); i++) {
var current = steps.get(i);
var next = i < steps.size() - 1 ? steps.get(i + 1) : new NoOpEnrichmentStep();
current.setNext(next);
}
chainHead = steps.get(0);
}
}
public Message enrich(Message message) {
return chainHead.enrich(message);
}
}
As a matter of fact, the EnrichmentStep
interface represents a generic chain element. So, we can encapsulate the code inside the ChainElement
interface directly. Check out the code snippet below.
public interface ChainElement<T> {
void setNext(T step);
static <T extends ChainElement<T>> T buildChain(List<T> elements, T lastElement) {
if (elements.isEmpty()) {
return lastElement;
}
for (int i = 0; i < elements.size(); i++) {
var current = elements.get(i);
var next = i < elements.size() - 1 ? elements.get(i + 1) : lastElement;
current.setNext(next);
}
return elements.get(0);
}
}
The buildChain
method accepts a list of business implementations that proceed with the actual use case and the stub one as the last element (i.e. NoOpEnrichmentStep
).
Now we can refactor the EnrichmentStepFacade
as well. Take a look at the code example below.
@Service
public class EnrichmentStepFacade {
private final EnrichmentStep chainHead;
public EnrichmentStepFacade(List<EnrichmentStep> steps) {
this.chainHead = ChainElement.buildChain(steps, new NoOpEnrichmentStep());
}
public Message enrich(Message message) {
return chainHead.enrich(message);
}
}
Much clearer and easier to understand.
AbstractEnrichmentStep
Anyway, there are still some caveats about the EnrichmentStep
implementation. Take a look at the PhoneNumberEnrichmentStep
definition below.
@Service
class PhoneNumberEnrichmentStep implements EnrichmentStep {
private final PhoneNumberRepository phoneNumberRepository;
private EnrichmentStep next;
@Override
public Message enrich(Message message) {
return message.getValue("SESSIONID")
.flatMap(phoneNumberRepository::findPhoneNumber)
.map(phoneNumber -> next.enrich(
message.with("phoneNumber", phoneNumber)
))
.orElseGet(() -> next.enrich(message));
}
public void setNext(EnrichmentStep step) {
this.next = step;
}
}
There are 2 details I want to point out:
- The
setNext
method overriding. Each implementation has to store the reference to the next chain element. - The
next.enrich(...)
method is called 2 times. So, the implementation has to repeat the contract requirements over and over again.
To eliminate these code smells, we declare the AbstractEnrichmentStep
class. Take a look at the code snippet below.
public abstract class AbstractEnrichmentStep implements EnrichmentStep {
private EnrichmentStep next;
@Override
public final void setNext(EnrichmentStep step) {
this.next = step;
}
@Override
public final Message enrich(Message message) {
try {
return enrichAndApplyNext(message)
.map(enrichedMessage -> next.enrich(enrichedMessage))
.orElseGet(() -> next.enrich(message));
}
catch (Exception e) {
log.error("Unexpected error during enrichment for msg {}", message, e);
return next.enrich(message);
}
}
protected abstract Optional<Message> enrichAndApplyNext(Message message);
}
Firstly, the next EnrichmentStep
is encapsulated within the AbstractEnrichmentStep
and the setNext
method is final. So, implementations don't bother about storing the further chain element.
Secondly, there is the new method enrichAndApplyNext
. As a matter of fact, an implementation doesn't have to worry about chaining nuances at all. If enrichment is successful, then the method returns the new message. Otherwise, Optional.empty
is retrieved.
And finally, the enrich
method is also final. Therefore, its implementation is fixed. As you can see, we enrichment algorithm is not duplicated across multiple classes but placed within a single method. It is simple:
- If
enrichAndApplyNext
returns the value, proceed it to the next enrichment step. - If no value is present, invoke the next step with the origin message.
- If any error occurs, log it and continue the enrichment chain further normally.
The last point is crucial. We don't know how many implementations there will be and what exceptions they may throw. Nevertheless, we don't want to stop the enrichment process entirely but just skip the failed chain block execution. Take a look at the PhoneNumberEnrichmentRepository
implementation below that extends the defined AbstractEnrichmentStep
.
@Service
class PhoneNumberEnrichmentStep extends AbstractEnrichmentStep {
private final PhoneNumberRepository phoneNumberRepository;
@Override
protected Optional<Message> enrichAndApplyNext(Message message) {
return message.getValue("SESSIONID")
.flatMap(phoneNumberRepository::findPhoneNumber)
.map(phoneNumber ->
message.with("phoneNumber", phoneNumber)
);
}
}
As you see, there is no infrastructure code anymore. Just pure business logic.
Points of improvement
If you have many enrichment steps, then certainly you want to monitor their activity.
- What time it takes to enrich the message on the particular step?
- What are the enrichment statistics? Which enrichments steps hit and miss most frequently?
- Which steps fail and why?
Metrics is the answer to all of these questions. Besides, the AbstractEnrichmentStep
declaration can also help us to record monitoring values clearer. Check out the code snippet down below.
public abstract class AbstractEnrichmentStep implements EnrichmentStep {
@Autowired
private MetricService metricService;
private EnrichmentStep next;
protected abstract EnrichmentType getEnrichmentType();
@Override
public final void setNext(EnrichmentStep step) {
this.next = step;
}
@Override
public final Message enrich(Message message) {
var start = System.nanoTime();
var type = getEnrichmentType();
try {
return enrichAndApplyNext(message)
.map(enrichedMessage -> {
metricService.recordHit(type);
return next.enrich(enrichedMessage);
})
.orElseGet(() -> {
metricService.recordMiss(type);
return next.enrich(message);
});
}
catch (Exception e) {
log.error("Unexpected error during enrichment for msg {}", message, e);
metricService.recordError(type, e);
return next.enrich(message);
}
finally {
var duration = Duration.ofNanos(System.nanoTime() - start);
metricService.recordDuration(type, duration);
}
}
protected abstract Optional<Message> enrichAndApplyNext(Message message);
}
First of all, there is a new abstract method getEnrichmentType()
. Each implementation should return its type to distinguish result metrics correctly.
Then the rules are these:
- If the message is successfully enriched, the
recordHit
method is called. - If the message enrichment is skipped, the
recordMiss
goes. - If any error occurs, then the
recordError
comes into play. - Finally the whole enrichment step duration is stored by
recordDuration
invocation.
You can tune the metrics the way you want. The idea is that the implementations don't care about those details. The Open-closed principle in action!
Conclusion
That's all I wanted to tell you about the Chain of Responsibility implementation in the Spring ecosystem. If you have any questions or suggestions, please leave your comments down below. Thanks for reading!
Resources
Posted on October 1, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.