Skip to content
ioob.dev
Go back

Design Patterns Part 4 — Template Method and Chain of Responsibility

· 7 min read
Design Pattern Series (4/8)
  1. Design Patterns Part 1 — Why Learn Patterns
  2. Design Patterns Part 2 — Strategy and State
  3. Design Patterns Part 3 — Observer and Command
  4. Design Patterns Part 4 — Template Method and Chain of Responsibility
  5. Design Patterns Part 5 — Factory Method, Abstract Factory, Builder
  6. Design Patterns Part 6 — Decorator and Proxy
  7. Design Patterns Part 7 — Adapter, Facade, Composite: Patterns for Organizing Structure
  8. Design Patterns Part 8 — Singleton, Iterator, Prototype: Frequently Used but Often Misunderstood
Table of contents

Table of contents

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.

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.

  1. 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.
  2. 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.

Comparing the Two Patterns Side by Side

Template Method and CoR take opposite approaches to the same problem — “controlling execution flow.”

CriterionTemplate MethodChain of Responsibility
Coupling mechanismInheritanceComposition (object linking)
Who determines the flowSuperclassChain assembly code
How to extendCreate a new subclassAdd a new handler to the chain
Changing the orderNot possible (superclass locks it)Possible (just reorder the assembly)
Runtime flexibilityLow (decided at compile time)High (chain can be reassembled at runtime)
Best suited forFrameworks with fixed algorithm skeletonsPipelines 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.

-> Part 5: Factory Method, Abstract Factory, Builder


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Design Patterns Part 3 — Observer and Command
Next Post
Design Patterns Part 5 — Factory Method, Abstract Factory, Builder