Data Access Patterns
Reading time: ~15 minutes
Introduction​
Data access is a critical aspect of Prism Architecture, providing the mechanisms for storing, retrieving, and manipulating data while maintaining separation between business logic and persistence concerns. This document explores the various data access patterns in Prism Architecture, how they interact with other layers, and when to use each approach.
The data access patterns in Prism Architecture are designed to balance several key considerations:
- Separation of domain logic from persistence details
- Optimization for different access patterns (CRUD operations vs. complex queries)
- Support for modern APIs like GraphQL alongside traditional REST APIs
- Maintaining clean architecture while allowing for performance optimization
Core Data Access Concepts​
Multiple Data Access Patterns​
Prism Architecture recognizes that different data access scenarios require different approaches. Rather than forcing all data access through a single pattern, it provides a suite of complementary patterns:
- Repository Pattern: Traditional entity-focused data access
- QueryService Pattern: Optimized for complex data retrieval scenarios
- Operation Pattern: Focused on specific GraphQL operations
These patterns work together to provide comprehensive data access capabilities while maintaining architectural integrity.
Comprehensive API Support​
While Prism Architecture was designed with GraphQL as a first-class citizen, it fully supports other API technologies as well. This flexible approach allows you to choose the most appropriate data access technology for each specific need.
REST API Support​
Prism Architecture provides comprehensive support for REST APIs:
- Standard Repository Implementations: Traditional repository implementations work perfectly with REST APIs
- RESTful Resource Organization: Repositories can be organized around REST resources
- HTTP Client Abstraction: Core Layer defines interfaces that abstract HTTP client details
- REST-Specific Folder Structure: Infrastructure Layer typically includes a dedicated REST folder structure:
REST/
├── Clients/
│ ├── RESTClient.swift
│ └── ...
├── Endpoints/
│ ├── UserEndpoints.swift
│ └── ...
Other Supported Data Sources​
Beyond GraphQL and REST, Prism Architecture supports various data sources:
- Local Storage: For on-device persistence using technologies like SQLite, Core Data, or Realm
- File Storage: For document-based or file system storage needs
- WebSocket APIs: For real-time communications and streaming data
- Third-party SDKs: For integration with vendor-specific APIs and services
- Legacy SOAP or XML-RPC Services: For integration with older enterprise systems
Hybrid Data Access Approaches​
Many applications benefit from using multiple data access approaches in concert:
- REST for Simple CRUD: Using REST for simple entity operations
- GraphQL for Complex Queries: Using GraphQL for data-intensive screens with complex relationships
- Local Storage for Offline Support: Using on-device storage to enable offline functionality
- WebSockets for Real-time Updates: Using WebSockets to complement REST/GraphQL with real-time data
Repository Pattern​
What is the Repository Pattern?​
The Repository Pattern is an abstraction that mediates between the domain model and data mapping layers. It provides a collection-like interface for accessing domain objects while hiding the complexities of database queries, API calls, or other data access mechanisms.
Purpose and Characteristics​
Repositories in Prism Architecture serve several critical purposes:
- Abstraction of Data Access: They hide the details of how data is stored and retrieved
- Domain-Centric Interface: They operate in terms of domain entities and concepts
- Separation of Concerns: They isolate data access logic from business logic
- Testability: They can be easily mocked or substituted for testing
Key Characteristics of Repositories​
- Aggregate-Based Organization: Typically organized around Aggregate Roots
- CRUD Operations: Focused on Create, Read, Update, Delete operations for entities
- Domain Integrity: Maintain the integrity and consistency of the domain model
- Interface in Core Layer: Defined as protocols/interfaces in the Core Layer
- Implementation in Infrastructure: Concrete implementations reside in the Infrastructure Layer
When to Use Repositories​
Repositories are the primary choice for:
- Standard entity CRUD operations
- Operations that must maintain aggregate boundaries
- When domain integrity is the primary concern
- For transactional operations that modify entity state
QueryService Pattern​
What is the QueryService Pattern?​
The QueryService Pattern is a specialized data access pattern in Prism Architecture optimized for complex data retrieval scenarios, particularly with GraphQL. Unlike repositories that focus on aggregate boundaries, QueryServices are designed for efficient data retrieval that may span multiple aggregates.
Purpose and Characteristics​
QueryServices address specific needs that traditional repositories don't handle optimally:
- Complex Query Optimization: Efficiently fetch complex, related data
- Cross-Aggregate Retrieval: Retrieve data spanning multiple aggregates in a single operation
- UI-Driven Data Needs: Organize around UI requirements rather than domain boundaries
- GraphQL Compatibility: Take advantage of GraphQL's ability to specify exactly what data to retrieve
Key Characteristics of QueryServices​
- Use Case or Feature Organization: Organized around specific use cases or features rather than entities
- Read-Optimized: Focused primarily on data retrieval, not modification
- DTO Return Values: Return Data Transfer Objects rather than domain entities
- Flattened Hierarchy: May flatten entity relationships for specific UI needs
- GraphQL Leverage: Utilize GraphQL's capabilities for efficient data fetching
When to Use QueryServices​
QueryServices are the preferred choice for:
- Complex data retrieval spanning multiple aggregates
- Screens or features with specific data requirements
- Performance-critical read operations
- GraphQL-based applications
- When aggregate boundaries would make data access inefficient
Operation Pattern​
What is the Operation Pattern?​
The Operation Pattern in Prism Architecture provides focused, purpose-specific GraphQL operations. Operations are granular, executable GraphQL queries or mutations that can be used by QueryServices or directly by the Orchestration Layer.
Purpose and Characteristics​
Operations serve several specific purposes:
- Encapsulate GraphQL Logic: Contain the specific GraphQL query or mutation
- Granular Functionality: Each operation typically handles one specific task
- Reusability: Can be combined and reused by multiple QueryServices
- Type Safety: Provide type-safe interfaces to GraphQL operations
Key Characteristics of Operations​
- Operation-Specific Organization: Organized by operation type (queries vs. mutations)
- Strongly Typed: Generate type-safe interfaces from GraphQL schema
- Implementation Details: Handle GraphQL-specific concerns like variables and fragments
- Composability: Can be composed into more complex operations
When to Use Operations​
Operations are typically used:
- As building blocks for QueryServices
- For simple, atomic GraphQL operations
- When type safety for GraphQL operations is important
- To avoid duplication of GraphQL query logic
Data Mapping​
The Role of Mappers​
Data mappers translate between domain entities and data transfer objects or external data formats. They are a crucial component in Prism Architecture's data access strategy.
Purpose and Characteristics​
Mappers serve several important purposes:
- Translation: Convert between domain entities and DTOs/external formats
- Boundary Protection: Shield domain model from external format changes
- Format Transformation: Handle serialization and deserialization
- Consistency: Ensure data integrity during transformation
Types of Mapping Scenarios​
- Entity to DTO: Map domain entities to DTOs for external communication
- DTO to Entity: Convert incoming data to domain entities
- Entity to Persistence Model: Transform entities for storage
- Persistence Model to Entity: Reconstruct entities from stored data
Relationship Between Data Access Patterns​
These patterns are not mutually exclusive but complementary, each serving different needs:
Repository and QueryService Collaboration​
- Repositories: Handle entity CRUD operations respecting aggregate boundaries
- QueryServices: Optimize complex data retrieval across aggregates
- Division of Responsibility: Repositories for writes, QueryServices for reads (in some applications)
QueryService and Operation Interaction​
- Operations: Provide the building blocks (GraphQL queries/mutations)
- QueryServices: Compose operations and provide a use case-focused interface
- Layered Approach: Operations are lower-level, QueryServices are higher-level abstractions
Data Access in Different Layers​
Core Layer​
- Defines repository interfaces
- Defines QueryService interfaces (when they're part of the core abstraction)
- Contains entity definitions that repositories work with
Domain Layer​
- Uses repositories through interfaces defined in the Core Layer
- Uses repositories via the Orchestration Layer, not directly
- Maintains domain integrity through domain-focused operations
- Does not typically interact directly with QueryServices
Orchestration Layer​
- Coordinates repository operations
- Uses QueryServices for data retrieval
- May use Operations directly for simple scenarios
- Manages transactions that span multiple repositories
- Acts as a mediator between Domain Layer and Infrastructure Layer
Infrastructure Layer​
- Implements repository interfaces
- Implements QueryServices
- Defines and implements Operations
- Contains data mapping logic
- Handles communication with data sources
Implementation Guidelines​
Repository Organization​
Repositories in Prism Architecture are organized primarily around Aggregates:
-
Aggregate-Based Repositories:
- Each Repository manages a complete Aggregate
- The Repository interfaces with the Aggregate Root entity
- All entities within an Aggregate are accessed through the Repository for that Aggregate
-
Independent Entity Repositories:
- Entities that aren't part of any Aggregate (or are themselves Aggregate Roots) can have dedicated repositories
- These repositories handle persistence operations for these standalone entities
QueryService Organization​
Unlike repositories, QueryServices don't need to strictly adhere to Aggregate boundaries:
-
Use Case or Feature-Based Organization:
- QueryServices are organized around specific use cases or features
- They're designed to efficiently fetch data needed for specific screens or components
- They can be optimized for particular data access patterns
-
Cross-Aggregate Queries:
- QueryServices can retrieve data that spans multiple Aggregates in a single operation
- They leverage GraphQL's ability to efficiently compose complex data queries
- This approach reduces the need for multiple separate repository calls
Operation Organization​
Operations are typically organized by their purpose and the entity they primarily work with:
-
Query vs. Mutation Separation:
- Query operations are separated from mutation operations
- This reflects the fundamental difference in their purpose
-
Entity-Focused Grouping:
- Operations are often grouped by the main entity they work with
- For example, UserQueries.swift might contain all query operations related to users
Specialized Data Access Patterns​
Read-Only Repositories​
For entities or projections that are never modified through the application:
- Focus exclusively on retrieval operations
- Often used for reference data or historical records
- Can be optimized for read performance
- May bypass some domain constraints for efficiency
Event-Sourced Repositories​
For domains using event sourcing:
- Store and retrieve event streams rather than entity state
- Reconstruct entities from their event history
- Support temporal queries (entity state at a specific point in time)
- Often paired with CQRS for read operations
GraphQL-Optimized QueryServices​
For applications using GraphQL:
- Support dynamic query composition
- Handle nested and related entity retrieval efficiently
- Map between GraphQL types and domain entities
- Address N+1 query problems through batching and dataloader patterns
Query Optimization Patterns​
Efficient data access is crucial for application performance. Prism Architecture recommends several patterns for query optimization:
Projection Queries​
Retrieve only the needed data fields rather than entire entities:
- Reduce data transfer overhead
- Minimize object mapping complexity
- Support specific view models or DTOs
- Can bypass domain constraints for read-only scenarios
Batch Loading​
Handle multiple related entities in a single operation:
- Solve the N+1 query problem
- Reduce database round trips
- Improve overall application performance
- Particularly important for GraphQL scenarios
Pagination​
Manage large result sets efficiently:
- Cursor-based pagination for consistent results
- Offset-based pagination for simplicity
- Keyset pagination for performance
- Include metadata about total results and available pages
Caching​
Strategic caching can dramatically improve performance:
- Entity caching for frequently accessed data
- Query result caching for expensive operations
- Cache invalidation strategies aligned with domain events
- Multi-level caching (memory, distributed, etc.)
Testing Data Access Components​
Repository Testing​
-
Unit Testing with Mocks:
- Test business logic that uses repositories without actual data access
- Mock repository interfaces to isolate domain logic
- Focus on correct behavior given different repository responses
-
Integration Testing:
- Test actual repository implementations against test databases
- Verify correct persistence and retrieval
- Test error handling and edge cases
QueryService Testing​
-
Unit Testing:
- Test mapping logic and query composition
- Mock underlying GraphQL operations
- Verify correct transformation of results
-
Integration Testing:
- Test against a test GraphQL endpoint
- Verify complex query scenarios work as expected
- Test error handling and performance
Operation Testing​
-
Unit Testing:
- Verify operation variables are correctly constructed
- Test response parsing
- Check error handling
-
Integration Testing:
- Execute operations against a test GraphQL endpoint
- Validate response handling
- Test with various input scenarios
Common Anti-Patterns to Avoid​
Repository Anti-Patterns​
- Repository Pollution: Adding methods that don't belong in a repository
- Leaky Abstractions: Exposing persistence details through the repository interface
- Over-Generalization: Creating repositories that are too generic
- Under-Abstraction: Not providing sufficient abstraction over data sources
QueryService Anti-Patterns​
- Domain Logic in QueryServices: Mixing domain rules with data retrieval
- Redundant Mapping: Unnecessary mapping between similar types
- One QueryService Per Screen: Creating too many granular QueryServices
- QueryServices That Modify Data: Using QueryServices for data modification
Operation Anti-Patterns​
- Monolithic Operations: Operations that fetch too much data
- Business Logic in Operations: Mixing business rules with GraphQL operations
- Redundant Operations: Creating similar operations with slight variations
- Insufficient Error Handling: Not properly handling GraphQL errors
Handling GraphQL and REST in the Same Application​
Many applications may need to support both GraphQL and REST APIs. Prism Architecture supports this through:
- Shared Abstractions: Use common interfaces when possible, with implementations for each API type
- Appropriate API Selection: Use GraphQL for complex, related data needs and REST for simpler CRUD operations
- QueryServices for GraphQL: Create specialized QueryServices for GraphQL that optimize data fetching
- Standard Repositories for REST: Use traditional repository implementations for REST APIs
- API-Specific Folders: Maintain separate folders for GraphQL and REST implementations to keep the code organized
Real-World Scenarios​
Scenario 1: E-Commerce Product Catalog​
Repository Approach:
ProductRepository
for CRUD operations on productsCategoryRepository
for category management- Maintains strict aggregate boundaries
QueryService Approach:
ProductCatalogQueryService
for efficiently retrieving products with categories, filters, and images- Optimized for product listing and details screens
- Returns flattened DTOs with exactly the needed data
Operations Used:
ProductQueries.swift
containing specific product-related GraphQL operations- Operations for listing, detail views, search results, etc.
Scenario 2: Social Media Feed​
Repository Approach:
PostRepository
for creating and updating postsUserRepository
for user profile managementCommentRepository
for comment management
QueryService Approach:
FeedQueryService
optimized for retrieving the feed with posts, comments, and user information- Returns ready-to-display feed items
Operations Used:
FeedQueries.swift
with operations for different feed typesPostMutations.swift
for actions like creating posts, liking, etc.
Conclusion​
Data access in Prism Architecture is designed to address the diverse needs of modern applications through complementary patterns. Repositories provide a clean abstraction for entity-focused operations that respect domain boundaries. QueryServices optimize for complex data retrieval scenarios, particularly with GraphQL. Operations provide granular, reusable GraphQL functionality.
Prism Architecture supports a wide range of data access technologies, including GraphQL, REST, local storage, WebSockets, and more. This flexibility allows you to choose the most appropriate technology for each specific data access need, while maintaining consistent architectural patterns.
By understanding when to use each pattern and how they work together, you can create a data access layer that maintains clean architecture while optimizing for performance and developer experience. These patterns allow your application to leverage the strengths of both traditional architectural approaches and modern technologies.
Remember that these patterns are not competing alternatives but complementary tools, each with specific strengths for different scenarios. The key is using them appropriately based on your specific data access requirements.
Next Steps​
- Use Cases and Business Logic - See how repositories and QueryServices are used within use cases
- GraphQL Integration - Explore specialized patterns for GraphQL data access in-depth
- Infrastructure Implementations - Learn how to implement these patterns with specific technologies
- Domain Services - Understand how domain services interact with repositories