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
2
3
4
5
Feed event received
-> decide whether to process it
-> route it to the right consumer
-> retry if something fails
-> notify other parts of the system

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
2
3
4
5
Request
-> AuthHandler
-> ValidationHandler
-> RateLimitHandler
-> BusinessHandler

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
2
3
ProcessFeedEventCommand
RetryProviderCommand
InvalidateCacheCommand

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
2
FeedBatch
-> next_event()

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
2
3
4
A -> B
A -> C
B -> C
C -> A

With a mediator:

1
2
3
A -> Mediator
B -> Mediator
C -> 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
2
3
EditorStateSnapshot
WorkflowStateSnapshot
ConfigSnapshot

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
2
3
4
FeedEventPublisher
-> IngestionConsumer
-> MetricsConsumer
-> DebugLogger

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
2
3
4
5
ConnectionState
-> Disconnected
-> Connecting
-> Connected
-> Failed

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
2
3
4
RetryStrategy
-> NoRetry
-> FixedDelayRetry
-> ExponentialBackoffRetry

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
2
3
4
5
process_event()
-> parse()
-> normalize()
-> validate()
-> publish()

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
2
3
4
5
6
7
8
9
Expression
-> Number
-> Add
-> Multiply

Visitors
-> EvaluateVisitor
-> PrintVisitor
-> OptimizeVisitor

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.

Reference