DEV Community 👩‍💻👨‍💻

Semyon Kirekov
Semyon Kirekov

Posted on • Updated on

Chain of Responsibility Pattern in Spring Application

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.

Meme article cover

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:

  1. Phone number determination by SESSIONID cookie.
  2. The person's age retrieving by the userId field.
  3. The person's geolocation obtaining by their IP address.

Development

Firstly, we need the EnrichmentStep interface.

public interface EnrichmentStep {
    Message enrich(Message msg);
}
Enter fullscreen mode Exit fullscreen mode

The interface accepts the current Message and returns the enriched one.

In this case, the Message class is immutable. Meaning that EnrichmentStep 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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now the PhoneNumberEnrichmentStep works a bit differently:

  1. If the message is successfully enriched, the fulfilled result proceeds to the next enrichment step.
  2. 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
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

And now the EnrichmentStep should extend it with the appropriate generic value.

public interface EnrichmentStep extends ChainElement<EnrichmentStep> {
  Message enrich(Message message);
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

There are 2 details I want to point out:

  1. The setNext method overriding. Each implementation has to store the reference to the next chain element.
  2. 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);
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. If enrichAndApplyNext returns the value, proceed it to the next enrichment step.
  2. If no value is present, invoke the next step with the origin message.
  3. 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)
        );
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

  1. What time it takes to enrich the message on the particular step?
  2. What are the enrichment statistics? Which enrichments steps hit and miss most frequently?
  3. 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);
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. If the message is successfully enriched, the recordHit method is called.
  2. If the message enrichment is skipped, the recordMiss goes.
  3. If any error occurs, then the recordError comes into play.
  4. 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

  1. The repository with the source code
  2. Null Object Design pattern
  3. Chain of Responsibility pattern explanation
  4. Immutability in Java
  5. Spring Application Context
  6. Spring @Order annotation
  7. The Open-closed principle

Top comments (0)

You can see total article reactions, views, and listing information by heading over to your dashboard.