Introduce Design Pattern III
In the previous article, we introduced Structural patterns. Structural patterns focus on how objects and modules are composed.
This article focuses on Behavioral patterns.
According to Refactoring.Guru, behavioral patterns focus on algorithms and responsibility assignment between objects.
In other words, behavioral patterns help us answer this question:
How should objects communicate, delegate work, and change behavior without making the code too tightly coupled?
Why behavioral patterns matter
In a real system, objects do not only hold data. They also make decisions, trigger actions, react to events, and coordinate with other objects.
For example:
1 | Feed event received |
If all decisions are written in one large function, the code becomes hard to test and hard to change.
Behavioral patterns help us separate these decisions and interactions into clearer responsibilities.
Behavioral patterns catalog
Refactoring.Guru lists ten common behavioral patterns:
| Pattern | Main idea | Common use case |
|---|---|---|
| Chain of Responsibility | Pass a request through a chain of handlers. | Validation, middleware, fallback handlers. |
| Command | Turn an action into an object. | Queues, undo/redo, background jobs. |
| Iterator | Traverse a collection without exposing internal structure. | Custom collections, streams, paginated results. |
| Mediator | Centralize communication between many objects. | Reducing direct dependencies between components. |
| Memento | Save and restore previous state. | Undo, snapshots, rollback. |
| Observer | Notify many listeners when something changes. | Event subscription, pub-sub, notifications. |
| State | Change behavior when internal state changes. | Lifecycle, workflow, connection states. |
| Strategy | Swap algorithms or decision behavior. | Routing, retry policy, fallback policy, pricing rules. |
| Template Method | Define algorithm skeleton and let subclasses fill steps. | Shared workflow with customizable steps. |
| Visitor | Add operations to object structures without changing the objects. | Operations over complex AST or object trees. |
Chain of Responsibility
Chain of Responsibility passes a request through multiple handlers until one handler processes it or all handlers finish.
For example:
1 | Request |
Each handler only needs to know the next handler.
Chain of Responsibility pros
- Each handler has a small responsibility.
- Handler order can be changed.
- New handlers can be added without changing existing handlers too much.
Chain of Responsibility cons
- Flow can become harder to trace.
- A request may pass through many layers before anything happens.
- Handler ordering bugs can be subtle.
Command
Command turns an operation into an object.
Instead of calling logic directly, we can package the action and execute it later.
For example:
1 | ProcessFeedEventCommand |
This is useful when actions need to be queued, logged, retried, scheduled, or undone.
Command pros
- Decouples request creation from execution.
- Works well with queues and background jobs.
- Makes retry and logging easier.
- Can support undo/redo if state is captured carefully.
Command cons
- Adds many small command types.
- Can be too heavy for simple direct calls.
- Requires clear ownership of command input and side effects.
Iterator
Iterator provides a standard way to traverse a collection without exposing its internal structure.
For example:
1 | FeedBatch |
The caller does not need to know whether the data comes from a vector, stream, cursor, or paginated API.
Iterator pros
- Hides collection internals.
- Provides a consistent traversal interface.
- Works well when the data source may change.
Iterator cons
- Can hide expensive operations such as network calls.
- Error handling can become more complex for async or streaming data.
- Not necessary for simple collections.
Mediator
Mediator centralizes communication between multiple objects.
Without a mediator, components may call each other directly:
1 | A -> B |
With a mediator:
1 | A -> Mediator |
This reduces direct dependencies between components.
Mediator pros
- Reduces many-to-many dependencies.
- Centralizes coordination logic.
- Makes components less aware of each other.
Mediator cons
- Mediator can become a god object.
- Coordination logic may become hidden.
- Not needed if components only have simple relationships.
Memento
Memento saves an object’s state so it can be restored later.
For example:
1 | EditorStateSnapshot |
This is useful for undo, rollback, or checkpoint behavior.
Memento pros
- Supports undo and restore.
- Keeps snapshot logic separate from business logic.
- Useful when state transitions are risky.
Memento cons
- Snapshots can consume memory.
- It can be hard to decide what state must be saved.
- Restoring old state can be dangerous if external side effects already happened.
Observer
Observer lets one object notify multiple listeners when something changes.
For example:
1 | FeedEventPublisher |
The publisher does not need to know every detail of each consumer.
Observer pros
- Decouples event producer and consumers.
- Makes it easier to add new consumers.
- Works well for event-driven systems.
Observer cons
- Event flow can become harder to trace.
- Error handling and retry behavior must be clear.
- Ordering and delivery guarantees can become important.
State
State changes an object’s behavior when its internal state changes.
For example:
1 | ConnectionState |
Each state can define what actions are allowed.
State pros
- Keeps state-specific behavior separate.
- Avoids large conditional blocks.
- Makes lifecycle transitions easier to reason about.
State cons
- Adds more types.
- Can be overkill for simple state flags.
- State transitions must be designed carefully.
Strategy
Strategy lets us swap one behavior or algorithm for another.
For example:
1 | RetryStrategy |
The caller uses the strategy interface without knowing which concrete behavior is selected.
Strategy pros
- Makes behavior replaceable.
- Keeps decision logic out of the caller.
- Works well with config-driven behavior.
- Makes each behavior easier to test.
Strategy cons
- Adds abstraction.
- Too many strategies can make the code harder to navigate.
- Should not be introduced before there are real alternative behaviors.
Template Method
Template Method defines the skeleton of an algorithm and lets specific steps be customized.
For example:
1 | process_event() |
Different implementations can override some steps while keeping the general flow.
Template Method pros
- Reuses common workflow structure.
- Keeps steps organized.
- Useful when several workflows share the same skeleton.
Template Method cons
- Often relies on inheritance, which can be rigid.
- The base workflow can become hard to change.
- Composition may be clearer in many modern codebases.
Visitor
Visitor adds operations to an object structure without changing the object types.
It is common when we have a stable object structure but want to add different operations over it.
For example:
1 | Expression |
Visitor pros
- Adds new operations without changing every object type.
- Keeps operation logic grouped together.
- Useful for complex object trees.
Visitor cons
- Adding new object types can become harder.
- The pattern can feel heavy.
- Usually unnecessary unless the object structure is stable and operations keep growing.
How to choose
Behavioral patterns should be chosen based on the interaction problem we actually have.
| Situation | Better pattern |
|---|---|
| A request should pass through multiple handlers | Chain of Responsibility |
| An action should be queued, logged, retried, or executed later | Command |
| A collection should be traversed without exposing internals | Iterator |
| Many components communicate too directly | Mediator |
| State needs to be saved and restored | Memento |
| One event should notify multiple consumers | Observer |
| Object behavior changes based on lifecycle state | State |
| One decision has multiple interchangeable behaviors | Strategy |
| Several workflows share the same skeleton | Template Method |
| Many operations run over a stable object structure | Visitor |
The main idea is that behavioral patterns help us manage responsibility. If every decision is placed in one large function, the system becomes hard to change. If every small decision becomes a pattern, the system becomes over-designed.
In the push feed case study, the most relevant behavioral patterns may be Observer, Strategy, and State, but only if the requirements prove they are needed.