Table of contents
- Who Holds the Flow
- Template Method — Fix the Skeleton, Swap the Details
- Chain of Responsibility — Flowing Requests Along a Chain
- Comparing the Two Patterns Side by Side
- Decision Criteria
Who Holds the Flow
When reading code, understanding “the order in which this logic executes” is often the key. Authenticate first, check permissions, then run the business logic. When this sequence is scattered across multiple places, changing one part can break another. A structure that controls the flow from a single point while keeping the individual steps flexible — that is exactly what the two patterns in this part aim for.
Template Method has the superclass define the algorithm’s skeleton while subclasses fill in the specific steps. Chain of Responsibility (CoR) links handler objects into a chain so that a request flows along it. The approaches are entirely different, but both solve the same problem: “controlling flow.”
Template Method — Fix the Skeleton, Swap the Details
Structure of the Pattern
The Template Method pattern belongs to the Behavioral patterns in the GoF (Gang of Four) design patterns. The core idea is simple. Place a method in the superclass that defines the overall sequence of the algorithm, and open each step as either abstract methods or hook methods. Subclasses only override those open parts.
classDiagram
class AbstractClass {
+templateMethod()
#step1()*
#step2()*
#hook()
}
class ConcreteClassA {
#step1()
#step2()
}
class ConcreteClassB {
#step1()
#step2()
#hook()
}
AbstractClass <|-- ConcreteClassA
AbstractClass <|-- ConcreteClassB
note for AbstractClass "templateMethod() locks the overall flow.\nstep1(), step2() are implemented by subclasses.\nhook() is optionally overridden."
templateMethod() is declared as final (or a non-open method in Kotlin) so that subclasses cannot change the order itself. What can change is only the implementation of individual steps.
Code Example — Data Processing Pipeline
Consider a scenario where you parse both CSV and JSON files. Both share the same skeleton: open file -> parse data -> validate -> return results. The only thing that differs is the parsing method.
Define the superclass in Java first.
public abstract class DataProcessor {
// Template method — locks in the sequence
public final List<Record> process(String filePath) {
String raw = readFile(filePath);
List<Record> records = parse(raw);
validate(records);
afterProcess(records); // Hook
return records;
}
private String readFile(String filePath) {
return Files.readString(Path.of(filePath));
}
// Step that subclasses must implement
protected abstract List<Record> parse(String raw);
// Default validation logic. Can be overridden if needed
protected void validate(List<Record> records) {
if (records.isEmpty()) {
throw new IllegalStateException("Parsing result is empty");
}
}
// Hook method — does nothing by default
protected void afterProcess(List<Record> records) {
// do nothing by default
}
}
What this class does is lock down the execution order inside process(). readFile is handled directly by the superclass, parse is delegated to subclasses. validate has a default implementation but can be overridden, and afterProcess is a hook.
Let us create a CSV processor.
public class CsvProcessor extends DataProcessor {
@Override
protected List<Record> parse(String raw) {
return Arrays.stream(raw.split("\n"))
.skip(1) // Skip header
.map(line -> {
String[] cols = line.split(",");
return new Record(cols[0].trim(), cols[1].trim());
})
.toList();
}
@Override
protected void afterProcess(List<Record> records) {
System.out.println("CSV processing complete: " + records.size() + " records");
}
}
A JSON processor follows the same structure.
public class JsonProcessor extends DataProcessor {
private final ObjectMapper mapper = new ObjectMapper();
@Override
protected List<Record> parse(String raw) {
try {
return mapper.readValue(raw, new TypeReference<>() {});
} catch (JsonProcessingException e) {
throw new RuntimeException("JSON parsing failed", e);
}
}
}
The JSON side does not override afterProcess. This is the reason hooks exist — only the subclasses that need them opt in, while the rest can ignore them.
What Is a Hook Method?
A hook is a method with an empty implementation. Unlike abstract methods, overriding is optional. It provides an extension point that says “if you want to insert something at this point, use this,” while still moving on with the default behavior (usually doing nothing) if it is not used.
Java’s HttpServlet is a classic example. The service() method determines the HTTP method and calls doGet(), doPost(), etc. — these are hooks. You only override the ones you need.
The Limitation of Being Inheritance-Based
Template Method’s weakness is its dependency on inheritance.
- Subclasses are tied to a single superclass. Since Java does not support multiple inheritance, a class cannot leverage two Template Methods simultaneously.
- Changing the superclass affects all subclasses. Adding a step or reordering them can break existing subclasses.
- Deep inheritance hierarchies make debugging harder. You need to follow
this.parse()through the IDE to find out which implementation it actually calls.
Because of these issues, combining with the Strategy pattern is a common alternative. Extract each algorithmic step into an interface, and have the executor receive injected implementations. However, when enforcing the order of steps is what truly matters, Template Method remains valid. It naturally appears when a framework “calls user code” — the so-called Inversion of Control (IoC).
Chain of Responsibility — Flowing Requests Along a Chain
Structure of the Pattern
The CoR pattern links handler objects that can process a request into a chain. When a request comes in, it starts with the first handler in the chain. If a handler cannot process it, it passes it to the next handler. Either someone handles it and the chain stops, or the request flows to the end and the final result comes back.
sequenceDiagram
participant Client
participant AuthHandler
participant LoggingHandler
participant RoleHandler
participant BusinessLogic
Client->>AuthHandler: Request
AuthHandler->>AuthHandler: Verify authentication
alt Authentication failed
AuthHandler-->>Client: 401 Unauthorized
else Authentication succeeded
AuthHandler->>LoggingHandler: Pass to next handler
LoggingHandler->>LoggingHandler: Log request
LoggingHandler->>RoleHandler: Pass to next handler
RoleHandler->>RoleHandler: Verify permissions
alt No permission
RoleHandler-->>Client: 403 Forbidden
else Has permission
RoleHandler->>BusinessLogic: Pass to next handler
BusinessLogic-->>Client: 200 OK
end
end
The key is that the client does not need to know “who handles it.” Adding handlers to the chain or reordering them does not touch the client code.
Code Example — Middleware Chain
The most common CoR example in practice is a web server middleware chain. Requests flow through authentication -> logging -> authorization check -> business logic.
First, define the common handler interface.
public interface Handler {
void setNext(Handler next);
void handle(Request request);
}
Create a base class to reduce boilerplate. It holds the reference to the next handler and handles the forwarding logic.
public abstract class BaseHandler implements Handler {
private Handler next;
@Override
public void setNext(Handler next) {
this.next = next;
}
protected void passToNext(Request request) {
if (next != null) {
next.handle(request);
}
}
}
What this class does is hold the next reference and, when passToNext() is called, invoke the next handler’s handle().
Now implement the authentication handler.
public class AuthHandler extends BaseHandler {
@Override
public void handle(Request request) {
String token = request.getHeader("Authorization");
if (token == null || !TokenValidator.isValid(token)) {
request.reject(401, "Authentication failed");
return; // Stop the chain
}
request.setUser(TokenValidator.extractUser(token));
passToNext(request); // Pass to next
}
}
The logging handler does not intercept the request — it just records it and always passes it along.
public class LoggingHandler extends BaseHandler {
private static final Logger log = LoggerFactory.getLogger(LoggingHandler.class);
@Override
public void handle(Request request) {
log.info("Request: {} {} (user={})",
request.getMethod(),
request.getPath(),
request.getUser());
passToNext(request);
}
}
The role-checking handler verifies whether a specific role is present.
public class RoleHandler extends BaseHandler {
private final String requiredRole;
public RoleHandler(String requiredRole) {
this.requiredRole = requiredRole;
}
@Override
public void handle(Request request) {
if (!request.getUser().hasRole(requiredRole)) {
request.reject(403, "Insufficient permissions");
return;
}
passToNext(request);
}
}
The chain assembly code looks like this.
Handler auth = new AuthHandler();
Handler logging = new LoggingHandler();
Handler role = new RoleHandler("ADMIN");
Handler business = new BusinessLogicHandler();
auth.setNext(logging);
logging.setNext(role);
role.setNext(business);
// The client only knows the first handler
auth.handle(incomingRequest);
This code connects four handlers in sequence, then the client sends the request to the chain’s entry point. Reordering handlers or inserting new ones does not require touching existing code.
More Concise with Kotlin
Using Kotlin’s functional features, you can implement CoR without a class hierarchy. Just define handlers as function types.
typealias Middleware = (Request, () -> Unit) -> Unit
fun authMiddleware(): Middleware = { request, next ->
val token = request.getHeader("Authorization")
if (token == null || !TokenValidator.isValid(token)) {
request.reject(401, "Authentication failed")
} else {
request.user = TokenValidator.extractUser(token)
next()
}
}
fun loggingMiddleware(): Middleware = { request, next ->
println("Request: ${request.method} ${request.path} (user=${request.user})")
next()
}
fun roleMiddleware(role: String): Middleware = { request, next ->
if (request.user?.hasRole(role) != true) {
request.reject(403, "Insufficient permissions")
} else {
next()
}
}
The chain assembly function also becomes clean. Use fold to wrap the middleware in reverse order.
fun buildChain(
middlewares: List<Middleware>,
finalHandler: (Request) -> Unit
): (Request) -> Unit {
return middlewares.foldRight(finalHandler) { middleware, next ->
{ request -> middleware(request) { next(request) } }
}
}
// Usage
val chain = buildChain(
listOf(authMiddleware(), loggingMiddleware(), roleMiddleware("ADMIN"))
) { request ->
// Business logic
request.respond(200, "Success")
}
chain(incomingRequest)
Instead of four classes, three functions and one assembly function produce the same structure. This is a spot where Kotlin’s strengths truly shine.
Two Variations of CoR
CoR has two major variations.
- Pure CoR: When one handler processes the request, the chain stops. A “find whoever can handle this event” form. Exception handling chains are this type.
- Pipeline CoR: Every handler does something to the request and passes it along. Middleware chains are this type. A handler may reject midway, but the intended behavior is generally to pass through all of them.
The middleware example above is closer to the pipeline form. It mixes handlers like the logging handler that always passes through, and the auth handler that conditionally stops the chain.
Where It Is Used in Practice
CoR is already embedded in many frameworks.
- Servlet Filters:
FilterChain.doFilter()is exactly the CoR structure. Authentication, CORS, and encoding filters form a chain. - Spring Security:
SecurityFilterChainruns multiple filters in sequence.UsernamePasswordAuthenticationFilter->BasicAuthenticationFilter->AuthorizationFilter, and so on. - OkHttp Interceptor: Network requests go through a chain of interceptors. Logging, retry, and auth token injection are implemented as interceptors.
- Kotlin’s coroutine
CoroutineExceptionHandler: Unhandled exceptions propagate up a handler chain.
Comparing the Two Patterns Side by Side
Template Method and CoR take opposite approaches to the same problem — “controlling execution flow.”
| Criterion | Template Method | Chain of Responsibility |
|---|---|---|
| Coupling mechanism | Inheritance | Composition (object linking) |
| Who determines the flow | Superclass | Chain assembly code |
| How to extend | Create a new subclass | Add a new handler to the chain |
| Changing the order | Not possible (superclass locks it) | Possible (just reorder the assembly) |
| Runtime flexibility | Low (decided at compile time) | High (chain can be reassembled at runtime) |
| Best suited for | Frameworks with fixed algorithm skeletons | Pipelines where processing steps change dynamically |
The common ground is that in both, you can execute the overall flow without knowing the implementation of individual steps. Template Method has the superclass guarantee this, and CoR has the chain structure guarantee it.
The key difference is inheritance versus composition. Template Method creates an IS-A relationship. CsvProcessor is a DataProcessor. CoR creates a HAS-A relationship. The auth handler has a next handler. This difference drives the difference in flexibility.
In practice, choose Template Method when the algorithm skeleton truly will not change, and CoR when the handler configuration needs to change at runtime. Of course, mixing both is also possible — using Template Method inside a handler, for instance.
Decision Criteria
When you are torn between the two patterns, ask this question:
“Will the order of algorithm steps absolutely never change?”
If so, Template Method is the simpler choice. If there are five steps and their order is fixed, locking the order in the superclass reduces mistakes.
Conversely, if some steps might be skipped, the order might change, or handlers might be added based on runtime configuration — CoR is the answer. Middleware chains are a classic case. You need the flexibility to include logging in development but exclude it in production.
Whichever pattern you choose, the goal is the same: keep control of the flow in one place, but make individual steps easy to swap out. Hold onto this principle, and both patterns will serve you well.
In the next part, we explore patterns that design the act of creating objects itself. We dig into why using new directly becomes a problem, how Factory Method and Abstract Factory separate creation logic, and how the Builder pattern assembles complex objects step by step.




Loading comments...