Composing Patterns
A guide to combining PatternKit patterns for complex, real-world solutions.
Why Compose Patterns?
Real-world problems rarely fit a single pattern. PatternKit's fluent API makes patterns composable by design—each pattern produces or consumes standard delegates, enabling seamless integration.
flowchart LR
subgraph Input
Request[Request]
end
subgraph Composition
Chain[Chain<br/>Route request]
Strategy[Strategy<br/>Select handler]
Decorator[Decorator<br/>Add behavior]
end
subgraph Output
Response[Response]
end
Request --> Chain
Chain --> Strategy
Strategy --> Decorator
Decorator --> Response
Composition Principles
1. Delegate Compatibility
Patterns produce delegates that other patterns consume:
// Strategy produces a Func<TIn, TOut>
var strategy = Strategy<Order, decimal>.Create()
.When(o => o.IsExpress).Then(ExpressShipping)
.Default(StandardShipping)
.Build();
// Decorator consumes and wraps it
var decorated = Decorator<Order, decimal>.Create(
order => strategy.Execute(order)) // Strategy as core
.After((order, cost) => ApplyDiscount(cost))
.Build();
2. Single Responsibility
Each pattern handles one concern:
| Pattern | Responsibility |
|---|---|
| Chain | Route to appropriate handler |
| Strategy | Select algorithm |
| Decorator | Add cross-cutting behavior |
| Proxy | Control access |
| Observer | Notify interested parties |
3. Composition Order Matters
// Logging sees cached results (cache hit = no "Calculating" log)
var cacheThenLog = Proxy<int, int>.Create(Calculate)
.CachingProxy() // Layer 1: Cache
.LoggingProxy(Log) // Layer 2: Log
.Build();
// Logging sees all calls (including cache hits)
var logThenCache = Proxy<int, int>.Create(Calculate)
.LoggingProxy(Log) // Layer 1: Log
.CachingProxy() // Layer 2: Cache
.Build();
Common Composition Patterns
Request Pipeline
Chain → Strategy → Decorator
Process requests through validation, route to appropriate handler, add cross-cutting concerns.
// 1. Chain: Validate and route
var validationChain = ResultChain<ApiRequest, ApiResponse>.Create()
.When(r => string.IsNullOrEmpty(r.ApiKey))
.Then(_ => new ApiResponse(401, "Unauthorized"))
.When(r => r.RateLimitExceeded)
.Then(_ => new ApiResponse(429, "Too Many Requests"))
.Build();
// 2. Strategy: Select handler based on endpoint
var routingStrategy = Strategy<ApiRequest, ApiResponse>.Create()
.When(r => r.Endpoint.StartsWith("/users"))
.Then(HandleUsersEndpoint)
.When(r => r.Endpoint.StartsWith("/orders"))
.Then(HandleOrdersEndpoint)
.Default(r => new ApiResponse(404, "Not Found"))
.Build();
// 3. Decorator: Add logging and metrics
var pipeline = Decorator<ApiRequest, ApiResponse>.Create(request =>
{
// Try validation first
if (validationChain.TryExecute(request, out var errorResponse))
return errorResponse;
// Route to handler
return routingStrategy.Execute(request);
})
.Before(request => Log($"Request: {request.Endpoint}"))
.After((request, response) => RecordMetrics(request, response))
.Around(AddTiming)
.Build();
// Usage
var response = pipeline.Execute(new ApiRequest("/users/123", apiKey));
Event-Driven Processing
Observer → Mediator → Command
Publish events, route to handlers, execute with undo support.
// 1. Observer: Event bus
var eventBus = Observer<DomainEvent>.Create().Build();
// 2. Mediator: Route events to handlers
var mediator = Mediator<DomainEvent, Unit>.Create()
.Handler<OrderCreated, Unit>(HandleOrderCreated)
.Handler<OrderShipped, Unit>(HandleOrderShipped)
.Handler<OrderCancelled, Unit>(HandleOrderCancelled)
.Build();
// 3. Command: Encapsulate operations with undo
var commandHistory = new Stack<ICommand<Order>>();
var createOrderCommand = Command<Order>.Create()
.Execute(order =>
{
order.Status = OrderStatus.Created;
eventBus.Publish(new OrderCreated(order));
})
.Undo(order =>
{
order.Status = OrderStatus.Draft;
eventBus.Publish(new OrderReverted(order));
})
.Build();
// Wire observer to mediator
eventBus.Subscribe(async evt => await mediator.Send(evt));
// Execute with history tracking
createOrderCommand.Execute(order);
commandHistory.Push(createOrderCommand);
// Undo if needed
if (commandHistory.Any())
{
var cmd = commandHistory.Pop();
cmd.Undo(order);
}
Factory with Prototype Templates
Factory → Prototype → Builder
Create by type, clone from templates, configure with builder.
// 1. Prototype: Define templates
var templates = Prototype<string, OrderConfig>.Create()
.Map("domestic", new OrderConfig
{
ShippingZone = Zone.Domestic,
Currency = "USD",
TaxRate = 0.08m
}, OrderConfig.Clone)
.Map("international", new OrderConfig
{
ShippingZone = Zone.International,
Currency = "USD",
TaxRate = 0.0m
}, OrderConfig.Clone)
.Build();
// 2. Factory: Create orders by type using templates
var orderFactory = Factory<string, Order>.Create()
.Map("domestic", () =>
{
var config = templates.Create("domestic");
return new Order(config);
})
.Map("international", () =>
{
var config = templates.Create("international");
return new Order(config);
})
.Build();
// 3. Builder: Configure the created order
var order = orderFactory.Create("domestic");
var configuredOrder = Builder<Order>.From(order)
.With(o => o.CustomerId, "CUST-001")
.With(o => o.Items, new[] { item1, item2 })
.With(o => o.Notes, "Rush delivery")
.Build();
Layered Access Control
Proxy → Decorator → Facade
Control access, add behavior, simplify interface.
// 1. Core service
PaymentResult ProcessPayment(PaymentRequest request) =>
paymentGateway.Process(request);
// 2. Proxy: Access control and caching
var securePayment = Proxy<PaymentRequest, PaymentResult>.Create(ProcessPayment)
.ProtectionProxy(req =>
authService.HasPermission(req.UserId, "payments:process"))
.CachingProxy() // Idempotency check
.Build();
// 3. Decorator: Add logging and retry
var robustPayment = Decorator<PaymentRequest, PaymentResult>.Create(
req => securePayment.Execute(req))
.Around((req, next) =>
{
for (int i = 0; i < 3; i++)
{
try { return next(req); }
catch (TransientException) when (i < 2)
{
Thread.Sleep(1000 * (i + 1));
}
}
throw new PaymentException("Max retries exceeded");
})
.After((req, result) => auditLog.Record(req, result))
.Build();
// 4. Facade: Simplified interface
var paymentFacade = Facade<PaymentCommand, PaymentResult>.Create()
.Operation("charge", cmd => robustPayment.Execute(
new PaymentRequest(cmd.Amount, cmd.CustomerId, PaymentType.Charge)))
.Operation("refund", cmd => robustPayment.Execute(
new PaymentRequest(cmd.Amount, cmd.CustomerId, PaymentType.Refund)))
.Operation("void", cmd => robustPayment.Execute(
new PaymentRequest(cmd.Amount, cmd.CustomerId, PaymentType.Void)))
.Build();
// Simple usage
var result = paymentFacade.Execute("charge", new PaymentCommand(99.99m, customerId));
Type-Safe Message Processing
TypeDispatcher → Strategy → Observer
Route by type, select processing strategy, notify subscribers.
// 1. Observer: Notification hub
var notifications = Observer<ProcessingResult>.Create().Build();
// 2. Strategy: Processing strategies per message type
var priorityStrategy = Strategy<EmailMessage, DeliveryPriority>.Create()
.When(m => m.Subject.Contains("URGENT")).Then(_ => DeliveryPriority.Immediate)
.When(m => m.Recipients.Count > 100).Then(_ => DeliveryPriority.Batch)
.Default(_ => DeliveryPriority.Normal)
.Build();
// 3. TypeDispatcher: Route messages by type
var messageProcessor = TypeDispatcher<IMessage, ProcessingResult>.Create()
.On<EmailMessage>(msg =>
{
var priority = priorityStrategy.Execute(msg);
var result = emailService.Send(msg, priority);
notifications.Publish(new ProcessingResult(msg.Id, result));
return result;
})
.On<SmsMessage>(msg =>
{
var result = smsService.Send(msg);
notifications.Publish(new ProcessingResult(msg.Id, result));
return result;
})
.On<PushNotification>(msg =>
{
var result = pushService.Send(msg);
notifications.Publish(new ProcessingResult(msg.Id, result));
return result;
})
.Default(msg =>
{
var result = new ProcessingResult(msg.Id, "Unknown message type");
notifications.Publish(result);
return result;
})
.Build();
// Wire up observers
notifications.Subscribe(r => logger.Log($"Processed: {r.MessageId}"));
notifications.Subscribe(
r => !r.Success,
r => alertService.Alert($"Failed: {r.MessageId}"));
// Process messages
foreach (var message in messageQueue)
{
messageProcessor.Dispatch(message);
}
State-Driven Workflow
State Machine → Observer → Command
Manage transitions, notify on state changes, support undo.
// 1. Observer: State change notifications
var stateChanges = Observer<StateChange<OrderState>>.Create().Build();
// 2. State Machine: Define valid transitions
var orderStateMachine = StateMachine<OrderState, OrderEvent>.Create()
.State(OrderState.Draft)
.On(OrderEvent.Submit).TransitionTo(OrderState.Pending)
.State(OrderState.Pending)
.On(OrderEvent.Approve).TransitionTo(OrderState.Approved)
.On(OrderEvent.Reject).TransitionTo(OrderState.Rejected)
.State(OrderState.Approved)
.On(OrderEvent.Ship).TransitionTo(OrderState.Shipped)
.On(OrderEvent.Cancel).TransitionTo(OrderState.Cancelled)
.State(OrderState.Shipped)
.On(OrderEvent.Deliver).TransitionTo(OrderState.Delivered)
.On(OrderEvent.Return).TransitionTo(OrderState.Returned)
.OnTransition((from, evt, to) =>
{
stateChanges.Publish(new StateChange<OrderState>(from, to, evt));
})
.Build();
// 3. Command: State changes with undo
var transitionHistory = new Stack<(Order, OrderState)>();
Command<Order> CreateTransitionCommand(OrderEvent evt) =>
Command<Order>.Create()
.Execute(order =>
{
var previousState = order.State;
if (orderStateMachine.TryFire(order.State, evt, out var newState))
{
transitionHistory.Push((order, previousState));
order.State = newState;
}
})
.Undo(order =>
{
if (transitionHistory.Any())
{
var (_, previousState) = transitionHistory.Pop();
order.State = previousState;
}
})
.Build();
// Wire observers
stateChanges.Subscribe(change =>
logger.Log($"Order state: {change.From} → {change.To}"));
stateChanges.Subscribe(
change => change.To == OrderState.Shipped,
change => emailService.SendShippingNotification(change));
// Usage
var submitCommand = CreateTransitionCommand(OrderEvent.Submit);
submitCommand.Execute(order);
Data Pipeline
Iterator/Flow → Decorator → Strategy
Stream data, transform it, apply processing strategies.
// 1. Strategy: Determine processing based on data
var processingStrategy = Strategy<DataRecord, ProcessingMode>.Create()
.When(r => r.Size > 1_000_000).Then(_ => ProcessingMode.Async)
.When(r => r.Priority == Priority.High).Then(_ => ProcessingMode.Immediate)
.Default(_ => ProcessingMode.Batch)
.Build();
// 2. Decorator: Add transformation layers
var transformer = Decorator<DataRecord, ProcessedRecord>.Create(
record => ProcessCore(record))
.Before(record => ValidateRecord(record))
.After((record, result) => EnrichWithMetadata(result))
.Build();
// 3. Flow: Stream processing
var pipeline = Flow<DataRecord>.From(dataSource)
.Filter(r => r.IsValid)
.Filter(r => !r.IsProcessed);
// Combine everything
foreach (var record in pipeline)
{
var mode = processingStrategy.Execute(record);
switch (mode)
{
case ProcessingMode.Immediate:
var result = transformer.Execute(record);
SaveImmediately(result);
break;
case ProcessingMode.Batch:
batchQueue.Enqueue(record);
break;
case ProcessingMode.Async:
_ = Task.Run(() => transformer.Execute(record));
break;
}
}
// Process batched items
foreach (var batch in batchQueue.GetBatches(100))
{
var results = batch.Select(r => transformer.Execute(r));
SaveBatch(results);
}
Visitor with Interpreter
Visitor → Interpreter → TypeDispatcher
Traverse structure, interpret expressions, dispatch by type.
// AST node types
abstract record Expression;
record NumberExpr(double Value) : Expression;
record BinaryExpr(Expression Left, string Op, Expression Right) : Expression;
record VariableExpr(string Name) : Expression;
// 1. TypeDispatcher: Evaluate nodes by type
TypeDispatcher<Expression, double> CreateEvaluator(Dictionary<string, double> variables)
{
TypeDispatcher<Expression, double> evaluator = null!;
evaluator = TypeDispatcher<Expression, double>.Create()
.On<NumberExpr>(n => n.Value)
.On<VariableExpr>(v => variables.GetValueOrDefault(v.Name, 0))
.On<BinaryExpr>(b =>
{
var left = evaluator.Dispatch(b.Left);
var right = evaluator.Dispatch(b.Right);
return b.Op switch
{
"+" => left + right,
"-" => left - right,
"*" => left * right,
"/" => right != 0 ? left / right : 0,
_ => 0
};
})
.Default(_ => 0)
.Build();
return evaluator;
}
// 2. Visitor: Collect all variables used
var variableCollector = Visitor<Expression>.Create()
.Visit<VariableExpr>((v, vars) =>
{
((HashSet<string>)vars).Add(v.Name);
})
.Visit<BinaryExpr>((b, vars, visit) =>
{
visit(b.Left, vars);
visit(b.Right, vars);
})
.Build();
// 3. Interpreter: Define evaluation rules
var interpreter = Interpreter<EvalContext, double>.Create()
.Terminal("evaluate", ctx =>
{
var evaluator = CreateEvaluator(ctx.Variables);
return evaluator.Dispatch(ctx.Expression);
})
.Build();
// Usage
var expression = new BinaryExpr(
new VariableExpr("x"),
"+",
new BinaryExpr(
new NumberExpr(10),
"*",
new VariableExpr("y")));
// Find required variables
var usedVars = new HashSet<string>();
variableCollector.Accept(expression, usedVars);
// usedVars = { "x", "y" }
// Evaluate with context
var context = new EvalContext(expression, new Dictionary<string, double>
{
["x"] = 5,
["y"] = 3
});
var result = interpreter.Execute(context); // 5 + (10 * 3) = 35
Composition Guidelines
Do
- Start simple: Begin with one pattern, add others as needed
- Document the flow: Diagram how patterns connect
- Test each layer: Verify each pattern works in isolation
- Consider performance: Composition adds overhead
Don't
- Over-compose: Not every problem needs multiple patterns
- Create circular dependencies: Patterns should flow one direction
- Mix sync and async carelessly: Use async variants throughout
- Ignore error handling: Each layer should handle its errors
Anti-Patterns
Composition Spaghetti
// Bad: Deeply nested, hard to follow
var result = decorator.Execute(
proxy.Execute(
chain.Execute(
strategy.Execute(
factory.Create(input)))));
// Better: Clear pipeline with named stages
var created = factory.Create(input);
var selected = strategy.Execute(created);
var validated = chain.Execute(selected);
var secured = proxy.Execute(validated);
var result = decorator.Execute(secured);
Redundant Layers
// Bad: Both add logging
var proxy = Proxy<int, int>.Create(Calculate)
.LoggingProxy(Log)
.Build();
var decorator = Decorator<int, int>.Create(x => proxy.Execute(x))
.After((x, r) => Log($"Result: {r}")) // Duplicate logging
.Build();
Wrong Pattern Order
// Bad: Validation after processing
var wrong = Decorator<Request, Response>.Create(Process)
.After((req, _) => Validate(req)) // Too late!
.Build();
// Good: Validation before processing
var correct = Decorator<Request, Response>.Create(Process)
.Before(req => Validate(req))
.Build();