Exploring the Chain of Responsibility Design Pattern with a Logging System in Java

Akshay Aryan
Java Developers Community
4 min readDec 31, 2023

--

Hey fellow developers! Today, I want to share my recent exploration into a fascinating design pattern that has significantly enhanced the flexibility and modularity of my code — the Chain of Responsibility pattern. To make things more relatable, I’ll walk you through a real-world scenario involving a logging system in Java.

The Quest for Flexibility: Chain of Responsibility Unveiled

In my constant pursuit of writing cleaner and more maintainable code, I stumbled upon the Chain of Responsibility pattern. It’s a behavioral design pattern that allows a request to pass through a chain of handlers, with each handler deciding whether it can process the request or pass it along to the next handler in the chain. This promotes a level of decoupling that piqued my interest, and I saw great potential for its application in a logging system.

The Logging System Conundrum

Consider a scenario where logs of varying severity levels need to be processed. Some logs are informational, others might be warnings, and then there are critical errors. Handling these logs in a scalable and maintainable way became a bit of a puzzle for me. Enter the Chain of Responsibility pattern.

Implementing the Chain of Responsibility in Java

I started by defining a LogHandler interface, representing the common behavior for log handlers. Each concrete handler—InfoHandler, WarningHandler, and ErrorHandler—implements this interface. The magic happens when I link these handlers together to create a chain. Each handler in the chain has the opportunity to process the log or pass it along to the next handler.

// LogHandler interface
interface LogHandler {
void handleLog(LogEntry log);
}

// LogEntry class
class LogEntry {
private String message;
private LogLevel severity;

public LogEntry(String message, LogLevel severity) {
this.message = message;
this.severity = severity;
}

public String getMessage() {
return message;
}

public LogLevel getSeverity() {
return severity;
}
}

// InfoHandler class
class InfoHandler implements LogHandler {
private LogHandler nextHandler;

@Override
public void handleLog(LogEntry log) {
if (log.getSeverity() == LogLevel.INFO) {
System.out.println("INFO: " + log.getMessage());
} else if (nextHandler != null) {
nextHandler.handleLog(log);
}
}

public void setNextHandler(LogHandler nextHandler) {
this.nextHandler = nextHandler;
}
}

// WarningHandler class
class WarningHandler implements LogHandler {
private LogHandler nextHandler;

@Override
public void handleLog(LogEntry log) {
if (log.getSeverity() == LogLevel.WARNING) {
System.out.println("WARNING: " + log.getMessage());
} else if (nextHandler != null) {
nextHandler.handleLog(log);
}
}

public void setNextHandler(LogHandler nextHandler) {
this.nextHandler = nextHandler;
}
}

// ErrorHandler class
class ErrorHandler implements LogHandler {
@Override
public void handleLog(LogEntry log) {
if (log.getSeverity() == LogLevel.ERROR) {
System.out.println("ERROR: " + log.getMessage());
} else {
System.out.println("Unhandled log severity: " + log.getSeverity());
}
}
}

Bringing the Concept to Life

To see this pattern in action, I created instances of the handlers, linked them together, and initiated the processing of log entries. It was like giving each handler a chance to inspect the log and decide whether it’s their responsibility or if it should be passed along the chain.

// Client class
public class ChainOfResponsibilityLoggingExample {
public static void main(String[] args) {
LogHandler infoHandler = new InfoHandler();
LogHandler warningHandler = new WarningHandler();
LogHandler errorHandler = new ErrorHandler();

// Set up the chain of responsibility
infoHandler.setNextHandler(warningHandler);
warningHandler.setNextHandler(errorHandler);

// Create log entries
LogEntry log1 = new LogEntry("This is an informational message.", LogLevel.INFO);
LogEntry log2 = new LogEntry("Warning: Potential issue detected.", LogLevel.WARNING);
LogEntry log3 = new LogEntry("Error: Critical error occurred!", LogLevel.ERROR);
LogEntry log4 = new LogEntry("Debug: Debugging information.", LogLevel.INFO);

// Process the log entries
infoHandler.handleLog(log1);
infoHandler.handleLog(log2);
infoHandler.handleLog(log3);
infoHandler.handleLog(log4);
}
}

Realizing the Power of Decoupling

What struck me most was how the Chain of Responsibility pattern effectively decoupled the log generation code from the handling logic. The client code responsible for creating log entries didn’t need to know which handler would ultimately process the log. Each handler had its own logic, and I could extend or modify the chain without touching the client code.

Conclusion: A Flexible Future

In conclusion, my journey with the Chain of Responsibility pattern has been enlightening. I can envision its applications not only in logging systems but also in various scenarios like event handling, request processing pipelines, and input validation systems. The level of flexibility and adaptability it brings to the table is truly empowering.

So, fellow developers, if you find yourself grappling with the complexities of request handling or event processing, consider giving the Chain of Responsibility pattern a shot. It might just be the missing piece in your quest for cleaner, more modular code.

Happy coding! 🚀

Thank you for being a part of our community! Before you go:

  • Be sure to clap, comment and follow 👏

--

--

Akshay Aryan
Java Developers Community

Cultivating a passion for technology and delving into intriguing, lesser-known tech facts.