Design Pattern Selection

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.

When to Select a Design Pattern

Identify the Problem Space

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.

Evaluate Patterns Against the Problem

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.

1. Benchmark with Real Data


$ ./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.

2. Define Evaluation Criteria

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.

3. Run Pattern Proofs-of-Concept (POCs)

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

Compare Patterns Using a Decision Matrix

Use a table format to compare patterns side-by-side. Each row is a criterion; each column is a pattern.

CriterionCQRSSagaEvent SourcingPipeline
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

Resolve Conflicting Tradeoffs

Patterns often conflict in their strengths. The key is to surface these tradeoffs explicitly.

1. Read-Heavy vs Write-Heavy Workloads

Choose CQRS when:

Choose Event Sourcing when:

2. Consistency Models

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}

3. When to Combine Patterns

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"

Document the Selection Process

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.

Failures That Happen Silently

When to Revisit the Selection

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

Final Checklist

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.