Module 4 · 4. Behavioral Patterns and Interview Practice · Lesson 11 of 12
Command, State, and Chain of Responsibility
Learning outcome
You will be able to: (1) encapsulate requests as Command objects so callers are decoupled from processing, (2) model context-dependent behaviour via the State pattern, and (3) compose modular processing steps with Chain of Responsibility so handlers can validate, log, and execute changes with clear separation of concerns.
Intuition
- Command: wrap a request (name + data + logic) into an object so you can queue, log, undo, and pass it to pipelines without tight coupling.
- State: store the Context's state as an object so behaviour changes by swapping the state instance rather than sprinkling conditionals across handlers.
- Chain of Responsibility: a linked pipeline of handlers that can choose to process, short-circuit, or pass along; good for validation, auth, logging, and transformation.
Deep dive
Key implementation roles:
- ICommand: small object representing a request; contains the data and minimal execute metadata.
- Handler (chain): receives ICommand + Context, performs a focused responsibility (validate/log/execute), and forwards to Next.
- Context + IState: represents operational mode; commands may cause transitions by asking the current state to handle a transition.
Why combine them? Commands decouple who asked from what is done; Chain composes orthogonal concerns (validation, tracing, business execution); State keeps business rules for transitions localized and testable. This makes pipelines extensible without modifying core business code.
Example pseudocode usage (conceptual):
Failure modes
- Over-chaining: creating many tiny handlers for trivial systems increases indirection and makes debugging harder.
- State explosion: modeling every combination of behaviour as a state object can become unwieldy; group related transitions and use predicates to limit states.
- Order-sensitivity bugs: Chain ordering matters (e.g., logging before validation may log invalid requests). Document expected ordering.
- Hidden side effects: Handlers that mutate context should do it explicitly and document transitions; prefer returning transition intents from business handlers rather than performing many in-flight mutations.
Interview drill
Q1: Why choose Command+Chain instead of a single service method? (Look for decoupling, testability, pipeline composition.) Q2: When would State be overkill? (Systems with only 1–2 boolean flags or small behavior differences.) Q3: How do you test a pipeline? (Unit-test handlers in isolation; use an in-memory context for integration tests; inject a terminal test handler.)
Micro-test (mental): Given handlers [Validation, Logging, Business], what happens if Validation rejects? (Expected: short-circuit; Logging and Business not executed.)
Revision checklist
- Commands are immutable or carry minimal mutable envelope.
- Handlers are single-responsibility and chainable.
- States encapsulate transition rules and are swapped on well-defined points.
- Side effects are minimized and documented.
- Ordering of handlers is well-documented and covered by tests.
Production code
When to prefer this composition over a simpler approach:
- Prefer when multiple orthogonal concerns must run around business logic (auth, validation, logging, metrics), and those concerns change independently.
- Avoid for microservices with a single, stable request flow; a direct service call is simpler.
Misuse warning: Don't use this pattern to hide large synchronous transactions across many handlers — prefer explicit orchestration or sagas for multi-step distributed work.
Below is a compact, deterministic, production-minded C# console example that materially demonstrates all three patterns together. It includes a classic extension method to run a Command through a chain and a comment showing how a future C# "extension(...)" block might express the same intent. The program prints deterministic lines so it is easy to test and reason about.
Code walkthrough
- ICommand: request object with Name (for deterministic logging).
- Context + IState: initial IdleState, ProcessingState, CompletedState; business handler asks current state whether a command can trigger a transition.
- Handlers: ValidationHandler, LoggingHandler, BusinessHandler form a chain. Validation always passes in this example to keep output deterministic.
- Extension method ExecuteThrough wires a command to a chain head for clarity.
Important: Every line of console output in the example is deterministic so the example is suitable for interview micro-tests and automated checks.
Below is the runnable source included in the exercise. Note the commented section that illustrates a hypothetical C# extension(...) block syntax (version-sensitive); the code uses a classic extension method for broad compatibility.
Exercises with explanations
1) Add an AuthorizationHandler that rejects commands when the context lacks roles. Test that Authorization short-circuits later handlers. Explanation: shows ordering importance. 2) Add a RetryHandler that re-invokes the terminal BusinessHandler on transient failures. Explanation: shows how a Chain can implement resilience. 3) Refactor state transitions so BusinessHandler returns Transition objects (instead of mutating context) and a coordinator applies them. Explanation: improves testability and separates concerns.
Interview: ask the candidate to extend the sample to support queuing commands and replaying them against a new Context instance to test idempotence.
Executable code examples
Command-State-Chain Pipeline Demo
Program.cs
using System;
// Why Command + State + Chain is preferable here: commands are decoupled, pipeline stages compose independently, and transitions are explicit and testable.
// Misuse warning: for one stable service call without independent stages or state transitions, this composition is unnecessary overhead.
// Small, deterministic demo combining Command, State, and Chain of Responsibility.
// ICommand: minimal request object
interface ICommand
{
string Name { get; }
}
record SetupCommand() : ICommand { public string Name => "Setup"; }
record WorkCommand() : ICommand { public string Name => "Work"; }
record FinishCommand() : ICommand { public string Name => "Finish"; }
// Context + State
class Context
{
private IState _state;
public Context(IState initial) => _state = initial;
public string StateName => _state.Name;
// Called by business logic to ask the state to handle a transition
public void ApplyTransition(Func<IState, IState?> transition)
{
var next = transition(_state);
if (next is not null)
_state = next;
}
}
interface IState { string Name { get; } }
class IdleState : IState { public string Name => "Idle"; }
class ProcessingState : IState { public string Name => "Processing"; }
class CompletedState : IState { public string Name => "Completed"; }
// Chain of Responsibility base
abstract class Handler
{
private Handler? _next;
public Handler? SetNext(Handler next)
{
_next = next;
return next;
}
public void Handle(ICommand cmd, Context ctx)
{
if (!Process(cmd, ctx))
return; // short-circuit
_next?.Handle(cmd, ctx);
}
// Return true if chain should continue
protected abstract bool Process(ICommand cmd, Context ctx);
}
class ValidationHandler : Handler
{
protected override bool Process(ICommand cmd, Context ctx)
{
// Deterministic: everything passes
Console.WriteLine($"ValidationHandler: {cmd.Name} validation OK");
return true;
}
}
class LoggingHandler : Handler
{
protected override bool Process(ICommand cmd, Context ctx)
{
Console.WriteLine($"LoggingHandler: {cmd.Name} logged");
return true;
}
}
class BusinessHandler : Handler
{
protected override bool Process(ICommand cmd, Context ctx)
{
// Business logic inspects command and may ask context to transition state
if (cmd is SetupCommand)
{
Console.WriteLine($"BusinessHandler: {cmd.Name} executed, transitioned {ctx.StateName}->Processing");
ctx.ApplyTransition(s => s is IdleState ? new ProcessingState() : null);
}
else if (cmd is WorkCommand)
{
Console.WriteLine($"BusinessHandler: {cmd.Name} executed in {ctx.StateName}");
// no transition
}
else if (cmd is FinishCommand)
{
Console.WriteLine($"BusinessHandler: {cmd.Name} executed, transitioned {ctx.StateName}->Completed");
ctx.ApplyTransition(s => s is ProcessingState ? new CompletedState() : null);
}
else
{
Console.WriteLine($"BusinessHandler: {cmd.Name} unknown - no-op");
}
return true; // continue if there were further handlers
}
}
// Classic extension method to execute a command through a chain head.
internal static class PipelineExtensions
{
internal static void ExecuteThrough(this ICommand cmd, Handler head, Context ctx)
{
head.Handle(cmd, ctx);
}
}
class Program
{
static void Main()
{
// Build a chain: Validation -> Logging -> Business
var validation = new ValidationHandler();
var logging = new LoggingHandler();
var business = new BusinessHandler();
validation.SetNext(logging).SetNext(business);
var context = new Context(new IdleState());
Console.WriteLine("=== Pipeline Run ===");
var commands = new ICommand[]
{
new SetupCommand(),
new WorkCommand(),
new FinishCommand()
};
foreach (var cmd in commands)
{
Console.WriteLine($"-- Command: {cmd.Name}");
cmd.ExecuteThrough(validation, context); // extension method
}
Console.WriteLine($"Final State: {context.StateName}");
}
}