In our journey through evolutionary architecture, we begin with the simplest yet effective pattern: Pure Handlers. These represent the foundational building blocks upon which more sophisticated architectural patterns will evolve.
Pure handlers embody a straightforward approach: one operation, one handler. They process a single request, perform necessary data manipulation, and return a result. Their beauty lies in their simplicity and directness—they offer a clear entry point for each distinct operation in your system.
As we explore this pattern, we'll use an e-commerce platform as our consistent example throughout this series. This will help us see how the same business domain can be implemented across different architectural generations.
Architectural Pattern Overview
Structure and Components
A pure handler architecture follows a simple structure:
Key components include:
Handlers: Self-contained units responsible for processing a specific request or command
Data Sources: Direct connections to databases, external services, or file systems
Request/Response DTOs: Simple data containers that define the input and output contracts
In our e-commerce example, we might have handlers like:
GetProductDetailsHandler
SearchProductsHandler
GetProductReviewsHandler
AddProductToCartHandler
Each handler is independent and unaware of the others, focused solely on its specific task.
Responsibilities and Relationships
In this pattern:
Each handler has a single responsibility—processing one type of request
Handlers typically access data sources directly
There's minimal abstraction between business logic and data access
Handlers are entry points, usually called directly from UI or API controllers
Change Frequency Characteristics
Pure handlers tend to have the following change patterns:
High Change Frequency: Since business logic and data access are combined, any change in either triggers a handler update
Coupled Changes: UI changes often necessitate corresponding handler modifications
Independent Evolution: Handlers can be modified without affecting other handlers
Stratification Analysis
Layer Organization
In the pure handlers pattern, the stratification is minimal:
We typically see just two primary layers:
Handler Layer: Contains request processing logic, validation, and business rules
Data Access Layer: Direct connections to data sources
This flatness is both a strength and limitation—it's simple to understand but offers limited separation of concerns.
Dependency Flow
The dependency flow is straightforward:
Handlers depend directly on their data sources with no additional abstractions in between. This creates a tight coupling that's simple to reason about but can become problematic as complexity grows.
Change Isolation
The primary change isolation mechanism in this pattern is the handler boundary itself:
Changes to one operation are isolated in its respective handler
Changes to shared data structures may affect multiple handlers
Database schema changes often cascade across multiple handlers
This provides a basic level of isolation that works well for simpler systems or bounded contexts with limited complexity.
Implementation Patterns
Key Interfaces and Components
The standard implementation includes:
Each handler implements this interface for its specific request and response types.
Common Variations
Several variations exist within this pattern:
Async Handlers: Using async/await patterns for I/O operations
Validated Handlers: Adding input validation before processing
Logged Handlers: Including standardized logging across operations
Authenticated Handlers: Adding permission checks before processing
Spoiler, some of the cross-cutting concerns will require a natural abstraction, and we will see it in action starting with Generation 2. They can also be abstracted and reused in this stage, but normally when you are using this generation, this is not a concern. But modern tooling made this very easy to fix (for logging, ILogger<T> [c#] and MDC [java]).
Technology-Agnostic Example
Let's consider a GetProductDetailsHandler
in our e-commerce system:
This handler would:
Receive a product ID in the request
Validate the input
Query the database directly for product details
Process the response
(optional) Transform database entities in to a response DTO
Return the product information
Complexity Management
Where Complexity Lives
In Pure Handlers, complexity is managed through:
Distribution: Each business operation has its own handler, spreading complexity across multiple small units
Isolation: Each handler addresses one specific concern
Simplicity: Direct data access with minimal abstraction layers
How the Pattern Distributes Complexity
This pattern excels at:
Separating operations from each other
Providing clear entry points for each use case
Simplifying debugging by keeping the execution path short and direct
However, it struggles with:
Shared business logic that crosses multiple handlers
Common data access patterns that get duplicated
Cross-cutting concerns that affect many handlers
Scaling Characteristics
Pure handlers scale reasonably well by operation count:
Adding new operations simply means adding new handlers
Existing handlers remain unaffected by new additions
Deployment can be granular, updating only affected handlers
However, they don't scale well with operation complexity:
As operations become more complex, handlers grow unwieldy
Shared logic leads to duplication or tangled dependencies
Cross-cutting concerns become increasingly difficult to manage consistently
The Three Dimensions Analysis
Coordination
Coordination in Pure Handlers is typically:
Self-contained: Each handler manages its own workflow
Procedural: Steps follow a linear sequence within each handler
Independent: No orchestration between handlers; each operates in isolation
In our e-commerce example, the AddProductToCartHandler
would coordinate all steps needed to add a product to a cart, from checking inventory to updating the cart, with no external coordination.
Communication
Communication patterns in Pure Handlers are:
Synchronous: Operations typically execute synchronously within the handler
Direct: No intermediaries between handler and data sources
Request/Response: Clear input and output contracts
For example, the SearchProductsHandler
would directly query the database and transform results to a response model without intermediate messaging.
Consistency
Consistency in Pure Handlers is managed through:
Transaction Scoping: The entire handler operation typically runs in a single transaction
Immediate Consistency: Changes are immediately visible after the handler completes
Local Enforcement: Each handler enforces its own consistency rules
Our e-commerce UpdateCartHandler
would manage consistency by wrapping its operations in a transaction, ensuring the cart is always in a valid state.
Dimensional Friction Points
The main friction points in Pure Handlers include:
Coordination-Communication Friction: As handlers grow, the lack of separation between coordination and communication becomes problematic
Consistency-Isolation Challenges: Having each handler manage its own transactions can lead to isolation issues in connected operations
Scale Limitations: The direct communication style doesn't scale well to highly complex operations
Testing Strategy
Testing Pyramid for Pure Handlers
The testing approach for Pure Handlers typically inverts the traditional testing pyramid:
More End-to-End Tests: Since handlers combine business logic and data access
Fewer Unit Tests: Limited opportunities for isolated unit testing
Integration-Heavy: Tests typically run against real or in-memory databases
Test Focus by Layer
Testing focuses on:
Handler Tests: Testing the complete request-to-response flow
Validation Tests: Ensuring inputs are properly validated
Edge Case Tests: Handling error conditions and boundaries
Test Isolation Strategies
Common isolation approaches include:
In-Memory Databases: For faster test execution
Database Transaction Rollbacks: To prevent test pollution
Test Containers: For more realistic but isolated testing
Common Testing Challenges
Challenges with testing Pure Handlers include:
Test Setup Complexity: Setting up the necessary database state
Slow Test Execution: Due to database dependencies
Overlapping Test Concerns: Tests often cover multiple responsibilities
When to Evolve
Signs Pure Handlers Are No Longer Sufficient
It's time to consider evolving beyond Pure Handlers when:
Duplication Increases: The same business logic appears in multiple handlers
Handler Size Grows: Individual handlers become too large and complex
Cross-Cutting Concerns Multiply: Common logic needs to be applied consistently
Testing Becomes Difficult: Setting up test cases becomes increasingly complex
Team Size Increases: Multiple developers need to work on related handlers
Triggers for Moving to Controllers
Specific triggers that indicate it's time to move to Controllers include:
Groups of handlers that operate on the same entity (e.g., multiple product-related operations)
Shared validation logic across multiple handlers
Related handlers that frequently change together
Transition Strategies
To evolve gracefully from Pure Handlers to Controllers:
Start by identifying groups of related handlers
Extract common validation and authorization logic (the cross-cutting part we mentioned at the beginning)
Create controllers that group related operations
Refactor handlers into controller methods
NFR Analysis
Non-Functional Requirement Alignment
Simplicity: ⭐⭐⭐⭐⭐ (Extremely straightforward implementation)
Maintainability: ⭐⭐⭐ (Good for small systems, degrades with size)
Testability: ⭐⭐ (Limited separation of concerns complicates testing)
Scalability: ⭐⭐⭐ (Scales well by operation count but not complexity)
Performance: ⭐⭐⭐⭐ (Direct access paths with minimal overhead)
Extensibility: ⭐⭐ (Limited extension points)
Strengths and Weaknesses
Strengths:
Fast implementation for new features
Clear operation boundaries
Easy to understand execution flow
Low technical complexity
Weaknesses:
Code duplication across handlers
Limited reuse of business logic
Tight coupling to data sources
Coupling between clients and logic
If no contract DTOs are used, coupling between clients and database structure
Growing maintenance burden as system expands
Optimization Opportunities
Even within the Pure Handlers pattern, several optimizations are possible:
Introduce basic cross-cutting concerns through decorators (or what the tech stack you are using has to offer)
Implement simple validation frameworks
Use code generation for repetitive handler patterns (ex: templates)
Consider handler middleware for common pre/post processing
Architect's Alert 🚨
Pure Handlers can quickly lead to the "copy-paste" architecture anti-pattern. While they're excellent for getting started and handling simple operations, be vigilant about duplication. When you find yourself copying logic between handlers, it's a strong signal that evolution to the next architectural generation is overdue.
Remember that Pure Handlers work best for:
Simple CRUD operations
Proof-of-concept implementations
Very small bounded contexts
Contexts with minimal business rules
They're less suitable for:
Complex domain logic
Operations that span multiple entities
High-volume changes to related functionality
Teams with multiple developers working on the same area
Conclusion and Next Steps
Pure Handlers represent the beginning of our architectural evolution journey. They provide a clean starting point with minimal complexity, clear boundaries, and straightforward implementation. Their simplicity makes them perfect for initial development and smaller bounded contexts.
However, as our e-commerce system grows, we'll need more sophisticated patterns to manage increasing complexity. In our next article, we'll explore how Controllers emerge as a natural evolution to group related operations and reduce duplication.
The journey from Pure Handlers to Controllers represents the first step in our architectural evolution—a step toward better organization, reduced duplication, and improved maintainability.
Questions for Reflection
In your current systems, where might Pure Handlers still be the right approach?
What operations in your domain are simple enough to benefit from this pattern?
Have you experienced the growing pains that signal the need to evolve beyond Pure Handlers?
How have you handled the transition from this pattern to more sophisticated approaches?