Support Ticket System
| Phase | Patterns | Project |
|---|---|---|
| 4 – Behavior | Chain of Responsibility, State | Support Ticket System |
Core Entities
| Entity | Role |
|---|---|
Ticket |
The subject — holds state, delegates behavior |
TicketPriority |
LOW, MEDIUM, HIGH — drives chain routing |
TicketState (enum) |
OPEN, IN_PROGRESS, WAITING_FOR_INPUT, RESOLVED, CLOSED |
TicketHandler |
Abstract base for the chain |
TicketStateHandler |
Interface for state behavior |
Chain of Responsibility Pattern
Problem it solves
A ticket comes in. You need to route it to the right handler based on priority. Without the pattern:
if (priority == LOW) {
bot.handle(ticket);
} else if (priority == MEDIUM) {
l1Agent.handle(ticket);
} else if (priority == HIGH) {
l2Specialist.handle(ticket);
}Adding a new priority level or a new handler = open this block and add a branch. The routing logic is coupled to every handler.
Solution
Each handler decides: "can I handle this? If yes, handle it. If no, pass to next."
public abstract class TicketHandler {
protected TicketHandler next;
public TicketHandler setNext(TicketHandler next) {
this.next = next;
return next; // returns next so chain can be built fluently
}
public abstract void handle(Ticket ticket);
protected void passToNext(Ticket ticket) {
if (next != null) {
next.handle(ticket);
} else {
System.out.println("[CHAIN] No handler could process: " + ticket.getId());
}
}
}Building the chain
BotHandler bot = new BotHandler();
bot.setNext(l1).setNext(l2).setNext(manager);
// entry point is always bot — the chain is transparent to the caller
handlerChain.handle(ticket);setNext() returns next (not void, not this) — enables fluent chaining on the next object in the sequence.
Routing result
LOW → [BOT] handles it directly
MEDIUM → [BOT] escalates → [L1 AGENT] handles it
HIGH → [BOT] escalates → [L1 AGENT] escalates → [L2 SPECIALIST] handles it
Why not just use if-else?
Adding a new priority (e.g., CRITICAL) with if-else = open the routing block and add a branch. With Chain of Responsibility = add one new handler class, insert it in the chain. The routing block (handlerChain.handle(ticket)) never changes.
State Pattern
Problem it solves
A ticket's valid operations depend on its current state. Without State pattern:
public void resolve() {
switch (this.state) {
case OPEN -> System.out.println("Cannot resolve — not assigned");
case IN_PROGRESS -> { this.state = RESOLVED; }
case WAITING_FOR_INPUT -> System.out.println("Cannot resolve — waiting for user");
case RESOLVED -> System.out.println("Already resolved");
case CLOSED -> System.out.println("Already closed");
}
}5 states × 4 methods = 20 switch branches in Ticket. Add a new state → touch all 4 methods. Add a new method → add another switch with 5 branches.
Solution
Each state is a class. The ticket holds the current state object and delegates all operations to it.
public interface TicketStateHandler {
void assign(Ticket ticket);
void waitForInput(Ticket ticket);
void resolve(Ticket ticket);
void close(Ticket ticket);
}Ticket delegates:
public void assign() { stateHandler.assign(this); }
public void resolve() { stateHandler.resolve(this); }Keeping enum and handler in sync
setState() updates both atomically — they can never diverge:
public void setState(TicketState state) {
this.state = state;
this.stateHandler = switch (state) {
case OPEN -> new OpenState();
case IN_PROGRESS -> new InProgressState();
case WAITING_FOR_INPUT -> new WaitingForInputState();
case RESOLVED -> new ResolvedState();
case CLOSED -> new ClosedState();
};
}Valid transitions
OPEN → IN_PROGRESS (assign)
IN_PROGRESS → WAITING (waitForInput)
IN_PROGRESS → RESOLVED (resolve)
WAITING → IN_PROGRESS (assign — user responded)
RESOLVED → CLOSED (close)
All other transitions print an error message. No exception needed — the state object handles the invalid call gracefully.
Illegal transition examples from output
[IN_PROGRESS] Already assigned. ← assign() called on IN_PROGRESS ticket
[CLOSED] Already closed. ← close() called on CLOSED ticket
The state object absorbs the call. The ticket never enters an invalid state.
Full Pattern Summary
| Pattern | Class | What it did |
|---|---|---|
| Chain of Responsibility | BotHandler, L1AgentHandler, L2SpecialistHandler, ManagerHandler |
Routed ticket to the right handler based on priority |
| State | OpenState, InProgressState, WaitingForInputState, ResolvedState, ClosedState |
Controlled what operations are valid at each lifecycle stage |
Chain of Responsibility vs State — Key Distinction
Both involve a sequence of decisions. The difference:
| Chain of Responsibility | State | |
|---|---|---|
| Question | "Who handles this?" | "What's valid right now?" |
| Direction | One request, passes until handled | Object changes behavior as it transitions |
| Once handled? | Chain stops | State changes, next call goes to new state |
| Structure | Linked list of handlers | Object holds current state object |
Recurring Theme Across All 4 Phases
Every pattern solved the same root problem: branching on type/state/priority.
| Anti-pattern | Replaced by |
|---|---|
if channel == EMAIL / SMS / PUSH |
Factory Method |
if (isSurge) / if (isLoyalty) |
Strategy |
if (priority == LOW / MEDIUM / HIGH) |
Chain of Responsibility |
switch (state) { case OPEN / IN_PROGRESS ... } |
State |
The fix is always the same: replace the branch with polymorphism. Each branch becomes a class. Adding a new case = adding a new class, never opening existing code.