Production-grade guide to design pattern selection covering architecture patterns, implementation strategies, testing approaches, and operational best practices for enterprise engineering teams.
Design pattern selection is the process of choosing the most appropriate structural or behavioral solution to a recurring software problem, balancing tradeoffs between complexity, performance, maintainability, and team expertise. It matters when a system grows beyond simple code organization — when teams face scalability, integration, or evolving requirements, and when decisions must be made across teams, services, or domains without relying on a single prescriptive architecture. This page guides you through the practical steps of selecting, evaluating, and justifying design patterns, with real tools and workflows you can apply immediately.
Begin by articulating the problem in terms of inputs, outputs, constraints, and failure modes. Use a structured format:
Problem: High-latency response times in a user profile service under load.
- Inputs: User ID, request headers, session context
- Outputs: Serialized profile data with nested preferences, recent activity
- Constraints: 99.9% availability, <200ms P95 latency, 10K RPS
- Failure modes:
- Profile fetch stalls when database is slow
- Caching layer misses cause backend cascades
- Profile data inconsistency across microservices
The problem must be specific enough to rule out broad patterns. A “slow API” problem might lead to either the CQRS or Saga pattern, but only when you’ve surfaced the exact pain points: data consistency, event-driven updates, and read-write asymmetry.
For each candidate pattern, extract its core mechanics and evaluate against your problem’s constraints. Use the following workflow with curl, psql, and systemctl to simulate real-world evaluation.
$ ./benchmark-profile-service.sh --users=10000 --duration=30s --profile-size=large
# Measure response times and error rates
$ curl -s http://profile-service:8080/metrics | grep 'http_response_time_seconds'
# Output: http_response_time_seconds{quantile="0.95"} 0.214
Use this to validate that a pattern is viable before full adoption.
Create a checklist of criteria that map directly to your constraints. Use a shared YAML file:
# evaluation_criteria.yml
- name: Latency
weight: 0.3
metric: p95_response_time_ms
target: <200
threshold: 250
description: "95th percentile response time under 10K RPS"
- name: Consistency
weight: 0.25
metric: eventual_consistency_delay
target: <1s
threshold: 2s
description: "Time from write to read consistency in distributed system"
- name: Scalability
weight: 0.2
metric: throughput_rps
target: 10000
threshold: 8000
description: "Maximum sustainable requests per second"
- name: Operational Complexity
weight: 0.15
metric: mean_time_to_recover
target: <15m
threshold: 30m
description: "Average time to resolve a failure"
- name: Development Velocity
weight: 0.1
metric: time_to_add_new_feature
target: <1 week
threshold: 2 weeks
description: "Time from idea to production deployment"
Apply this to each pattern candidate.
Use a consistent POC structure. Each pattern is evaluated via a minimal but realistic implementation.
For CQRS (Command Query Responsibility Segregation):
# 1. Set up command and query databases
$ psql -c "CREATE TABLE commands (id UUID PRIMARY KEY, command_type TEXT, payload JSONB);"
$ psql -c "CREATE TABLE user_profiles (id UUID PRIMARY KEY, name TEXT, preferences JSONB, last_updated TIMESTAMPTZ);"
# 2. Implement command handler
$ cat commands/UpdateUserProfileHandler.go
package commands
import (
"context"
"encoding/json"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
type UpdateUserProfileCommand struct {
UserID string `json:"user_id"`
Name string `json:"name"`
Preferences map[string]interface{} `json:"preferences"`
}
func (h *UpdateUserProfileHandler) Handle(ctx context.Context, cmd []byte) error {
var command UpdateUserProfileCommand
if err := json.Unmarshal(cmd, &command); err != nil {
return err
}
_, err := h.db.ExecContext(ctx, `
INSERT INTO commands (id, command_type, payload)
VALUES ($1, $2, $3)
`, uuid.New(), "UpdateUserProfile", cmd)
return err
}
For Saga Pattern:
# 1. Define saga state machine
$ cat sagas/profile-update-saga.yaml
---
name: profile-update
steps:
- name: create-user
service: user-service
action: create
input: user_id, name
output: user_created
compensation: user-service/delete
- name: update-preferences
service: preferences-service
action: set
input: user_id, preferences
output: preferences_updated
compensation: preferences-service/reset
- name: notify-user
service: notification-service
action: send
input: user_id, message
output: notification_sent
compensation: notification-service/undo
Use systemctl to start and monitor sagas:
$ systemctl start profile-saga-worker
$ systemctl status profile-saga-worker
# Output: Active: active (running) since Mon 2024-05-20 10:00:00 UTC; 2h 15min ago
Use a table format to compare patterns side-by-side. Each row is a criterion; each column is a pattern.
| Criterion | CQRS | Saga | Event Sourcing | Pipeline |
|---|---|---|---|---|
| Latency | ✅ <200ms | ✅ <250ms | ✅ <300ms | ✅ <150ms |
| Consistency | ✅ Eventual | ✅ Strong | ✅ Exactly-once | ✅ At-least-once |
| Scalability | ✅ High | ✅ Medium | ✅ High | ✅ High |
| Operational Complexity | ✅ Medium | ✅ High | ✅ High | ✅ Low |
| Development Velocity | ✅ Medium | ✅ High | ✅ Low | ✅ High |
Use grep, awk, and jq to extract and compare results:
$ grep -r "p95" ./results/ | jq -r '.p95_response_time_ms'
# Output: 198, 214, 267, 142
$ awk '/CQRS/ {print "CQRS:", $3}' ./results/summary.csv
# Output: CQRS: 198
Patterns often conflict in their strengths. The key is to surface these tradeoffs explicitly.
Choose CQRS when:
Choose Event Sourcing when:
Use psql and curl to validate consistency:
# Simulate a failure during a saga
$ curl -X POST http://saga-worker:8080/sagas/profile-update/start \
-d '{"user_id": "a1b2c3", "name": "Alice"}'
# Kill preferences-service during execution
$ systemctl stop preferences-service
$ curl -s http://saga-worker:8080/sagas/profile-update/status | jq '.status'
# Output: {"status": "failed", "compensation_started": true}
Use a docker-compose.yml to run a combined system:
version: '3.8'
services:
profile-service:
build: .
depends_on:
- postgres
- saga-worker
environment:
- EVENT_STORE_URL=http://event-store:8080
- SAGA_WORKER_URL=http://saga-worker:8080
saga-worker:
image: saga-worker:latest
volumes:
- ./sagas:/app/sagas
command: ["--config=/app/sagas/profile-update.yaml"]
event-store:
image: confluentinc/cp-kafka:7.0
ports:
- "9092:9092"
The final decision must be justifiable. Use a template:
# Design Pattern Selection: Profile Service
## Problem Statement
High-latency response times in a user profile service under load.
## Candidate Patterns
- CQRS
- Saga
- Event Sourcing
- Pipeline
## Evaluation Criteria
- Latency (P95 < 200ms)
- Consistency (eventual < 1s)
- Scalability (10K RPS)
- Operational Complexity (MTTR < 15m)
- Development Velocity (< 1 week)
## Results Summary
| Pattern | P95 Latency | Consistency Delay | RPS | MTTR | Time to Feature |
|-----------------|-------------|-------------------|-----|------|-----------------|
| CQRS | 198 | 800 | 11k | 12m | 5d |
| Saga | 214 | 1.2s | 9.5k| 25m | 4d |
| Event Sourcing | 267 | 950 | 8.2k| 30m | 9d |
| Pipeline | 142 | 600 | 12k | 8m | 6d |
## Decision
Choose **CQRS** for primary pattern.
### Rationale
- Best latency and scalability.
- Matches existing read-heavy access patterns.
- Supports future extensions like caching layers and materialized views.
- Team familiarity with event-driven systems.
### Risks and Mitigations
- **Risk**: Write-side latency increases due to event publishing.
- **Mitigation**: Use async event publishing with batching.
- **Risk**: Read model lag during peak load.
- **Mitigation**: Add read replicas and optimize projections.
### Acceptance Criteria
- Profile fetch latency < 200ms at 10K RPS (P95).
- 99.9% availability over 30 days.
- Ability to add new profile fields in < 3 days.
projection_lag_seconds metric and alert on >5min.UserCreated event but old readers don’t handle it.event_version field; validate on consumer startup.saga_id, step, status, last_updated.command_id field; deduplicate at the command handler.Re-evaluate the selected pattern when:
Use systemctl status and journalctl to track pattern health:
$ journalctl -u profile-service --since "2024-05-20" | grep -i "event"
# Output: Event created: user_id=a1b2c3, event_type=ProfileUpdated
Design pattern selection is not a one-time activity. It is the foundation of architectural maturity — where teams move from solving problems to choosing solutions with intention, clarity, and confidence.
This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy