Skip to main content

Key Implementation Decisions

Reading time: ~12 minutes

Introduction​

When implementing Prism Architecture, developers face several critical decisions that significantly impact the application's structure, maintainability, and development experience. These decisions shape how the architecture is realized in code and influence the team's workflow throughout the project's lifecycle.

This document outlines the key implementation decisions developers need to make when adopting Prism Architecture. Understanding these decision points will help you tailor the architecture to your specific project needs while maintaining its core principles and benefits.

Architectural Boundary Decisions​

Project Structure Approach​

One of the first and most impactful decisions is how to physically organize your Prism Architecture in terms of project structure:

  1. Swift Package Per Layer: Strict separation with each architectural layer as an independent package

    • Provides strong architectural enforcement through explicit dependencies
    • Enables focused work with clear boundaries between layers
    • Increases initial complexity and setup overhead
    • Best for larger teams and enterprise applications
  2. Monolithic Project Structure: All layers within a single project structure

    • Simpler setup and management
    • Easier refactoring between layers
    • Relies on developer discipline to maintain boundaries
    • Best for smaller teams and applications
  3. Hybrid Approaches:

    • Foundation Packages: Core and Common layers as packages, other layers monolithic
    • Feature-Based Packages: Features as vertical slices across all layers
    • Balances architectural enforcement with development flexibility
    • Good for medium-sized applications and teams

The decision should be based on your team size, project complexity, reusability needs, and the importance of enforcing architectural boundaries:

Considerations:

  • Team size and organization
  • Project size and complexity
  • Architectural enforcement priority
  • Development velocity requirements
  • Reusability needs

Layer Visibility Rules​

Prism Architecture has specific rules about which layers can access others. You must decide how strictly to enforce these rules:

  1. Compiler-Enforced Boundaries: Using project structures or access modifiers to enforce layer access rules

    • Prevents accidental violations
    • Makes architectural boundaries explicit
    • May increase development friction
  2. Convention-Based Boundaries: Relying on team discipline and code reviews

    • More flexible for evolving architectures
    • Lower setup complexity
    • Requires diligent review processes
  3. Hybrid Enforcement: Enforcing critical boundaries in compilation, others by convention

    • Balances flexibility and architectural integrity
    • Focuses enforcement on the most important boundaries

The standard layer access rules in Prism Architecture are:

Inter-Layer Communication Decisions​

Intent and State Management​

A critical decision in Prism Architecture is how to structure the Intent and State objects used for communication between layers:

  1. Common Layer Definition Approach (Recommended):
    • Define all Intent and State types in the Common Layer
    • Organize in a clear folder structure by purpose and usage
    • Allow all layers to access these definitions
    • Create actual objects in the originating layers at runtime
  1. Layer-Specific Approach (Not Recommended):
    • Each layer defines its own communication types
    • Creates dependency issues between layers
    • Makes it harder to track communication flow
    • Leads to inconsistent patterns

The recommended approach uses a clear folder structure in the Common Layer:

Common/
├── Intent/
│ ├── Base/ # Base interfaces
│ ├── UI/ # UI-specific intents
│ ├── Application/ # Application intents
│ └── Data/ # Data operation intents
└── State/
├── Base/ # Base interfaces
├── UI/ # UI-specific states
├── Application/ # Application states
└── Response/ # Response states

This ensures consistent communication patterns while maintaining clean layer boundaries.

Communication Flow Strategy​

Another key decision is how data and requests flow between layers:

  1. Package Delivery Model (Recommended):
    • Communication objects (Intent and State) act like delivery packages
    • Created in the originating layer using templates from Common
    • Pass through the architecture to their destinations
    • Each layer extracts only what it needs
    • Objects remain immutable during transit
  1. Complete Transformation Model:

    • Each layer boundary requires full transformation
    • More isolation but higher transformation overhead
    • Creates more mapping code
  2. Mixed Flow Model:

    • Some boundaries use full transformation
    • Others use package delivery
    • More complex mental model

The recommended package delivery model creates efficiency while maintaining proper boundaries.

Core Layer Decisions​

Entity Mutability​

Decide whether entities in the Core Layer will be:

  1. Immutable Entities: Entities as immutable value types (structs in Swift)

    • Prevents unexpected state changes
    • Thread-safe by default
    • Requires new instances for any changes
    • Follows functional programming principles
  2. Mutable Entities: Entities as mutable reference types (classes in Swift)

    • Allows in-place modifications
    • More memory-efficient for complex objects
    • Requires careful management of state changes
    • Better for complex object graphs
  3. Hybrid Approach: Immutable public interface with controlled internal mutability

    • Provides immutability guarantees externally
    • Enables efficient internal operations
    • More complex implementation

This decision impacts how entities are handled throughout the application and the programming paradigms you'll employ.

ID Management Strategy​

Decide how entity IDs will be generated and managed:

  1. Client-Generated IDs: Generate IDs in the client application

    • Enables offline operation
    • Simplifies creating new entities
    • May require conflict resolution strategies
  2. Server-Generated IDs: IDs assigned by the backend

    • Guarantees uniqueness within the system
    • Simplifies server-side implementation
    • Requires additional roundtrips for entity creation
  3. Hybrid ID Strategy: Different strategies for different entity types

    • Balances needs across entity types
    • May allow temporary IDs until persisted

This decision affects how entities are created, persisted, and synchronized with backend systems.

Protocol vs. Concrete Types​

Decide when to use protocols versus concrete types in your Core Layer:

  1. Protocol-Heavy Approach: Define protocols for most core components

    • Maximizes flexibility and substitutability
    • Enables easier testing with mocks
    • Increases abstraction complexity
  2. Concrete-Type Approach: Use concrete types for most components

    • Simplifies understanding and navigation
    • Reduces abstraction overhead
    • Makes substitution more challenging
  3. Strategic Protocol Usage: Use protocols only where substitution is needed

    • Balances abstraction and concreteness
    • Focuses flexibility where it matters most
    • Reduces unnecessary abstraction

This decision influences how flexible and testable your Core Layer will be.

Domain Layer Decisions​

Validation Strategy​

Decide how domain validation will be structured and implemented:

  1. Entity-Internal Validation: Validation logic within entities

    • Ensures entities are always valid
    • Simplifies usage by guaranteeing validity
    • May complicate entity creation and updates
  2. Separate Validation Services: Dedicated services for validation

    • Keeps entities focused on data and basic behavior
    • Allows for context-dependent validation
    • Enables reuse of validation logic across contexts
  3. Combined Approach: Basic validation in entities, complex validation in services

    • Ensures fundamental integrity in entities
    • Allows for flexible contextual validation
    • Balances responsibilities

This decision affects how business rules are enforced and where validation logic resides in your application.

Domain Event Handling​

Decide how to implement domain events:

  1. Synchronous Domain Events: Events processed immediately

    • Simplifies implementation and debugging
    • Ensures immediate consistency
    • May create tight coupling between components
  2. Asynchronous Domain Events: Events queued for later processing

    • Better performance for the triggering operation
    • More complex implementation
    • May introduce eventual consistency challenges
  3. Hybrid Event Handling: Critical events processed synchronously, others asynchronously

    • Balances consistency and performance
    • Allows optimization based on specific needs
    • More complex event handling system

This decision impacts how reactive and responsive your domain model will be to changes.

Orchestration Layer Decisions​

Use Case Input/Output Format​

Decide on the structure of use case inputs and outputs:

  1. Intent/State Pattern (Recommended): Use Intent for input and State for output
    • Aligns with the overall communication strategy
    • Creates consistent patterns across the application
    • Enhances traceability of operations
    • Uses Common Layer definitions
  1. Simple Parameter Lists: Direct parameters and return values

    • Less code overhead
    • May be less clear for complex operations
    • More prone to parameter order errors
  2. Generic Command Pattern: Generic command objects processed by use cases

    • Uniform handling of all operations
    • Supports undo/redo and command logging
    • More complex implementation

The recommended Intent/State pattern aligns with the overall communication approach of Prism Architecture.

Error Handling Strategy​

Decide how errors will be handled and communicated in the Orchestration Layer:

  1. State-Based Error Communication: Include error information in State objects

    • Consistent with the overall communication pattern
    • Makes error handling explicit and predictable
    • Treats errors as normal application flow
  2. Domain-Specific Error Types: Custom error types for domain errors

    • Communicates domain concepts clearly
    • Enables precise error handling
    • Increases error type proliferation
  3. Generic Error Categories: Broader error categories with details

    • Reduces number of error types
    • Simplifies error handling code
    • May lose some domain specificity
  4. Result Types: Use of Result or similar types instead of throws

    • Makes error handling explicit in function signatures
    • Enables more functional error handling
    • Different from standard Swift error handling

The choice should align with your overall communication strategy and error handling philosophy.

Orchestration Hierarchy Implementation​

Decide how to implement the hierarchical orchestration structure:

  1. Strict Hierarchy: Clearly defined Use Cases, Orchestration Services, and Workflows

    • Strong enforcement of the hierarchical structure
    • Clear organization by complexity
    • May feel overly rigid for simpler features
  2. Flexible Hierarchy: Less strict boundaries between orchestration levels

    • Adapts to varying feature complexity
    • May lead to less consistent organization
    • Easier for developers to understand initially
  3. Progressive Hierarchy: Start simple, evolve to more structure as needed

    • Matches structure to current complexity
    • Allows natural evolution as features mature
    • Requires refactoring discipline

This decision shapes how your orchestration components interact and evolve over time.

Infrastructure Layer Decisions​

Persistence Technology​

Decide which persistence technologies to use:

  1. REST APIs: Traditional REST endpoints

    • Widely supported
    • Simpler implementation
    • Less efficient for complex data needs
  2. GraphQL: Schema-based flexible querying

    • Optimized for complex, related data
    • Reduces over-fetching and under-fetching
    • More complex server implementation
  3. Local Storage: On-device persistence

    • Enables offline operation
    • Faster data access
    • Requires synchronization strategies
  4. Hybrid Approaches: Combination of multiple technologies

    • Maximizes benefits of each approach
    • More complex implementation
    • Leverages appropriate technology for each need

This decision affects your data access patterns and overall application performance.

Repository Implementation Strategy​

Decide how repositories will be implemented:

  1. Traditional Repositories: Focused on aggregate boundaries

    • Preserves domain integrity
    • Clear responsibilities
    • May be less efficient for complex queries
  2. Query Services: Optimized for complex data access

    • More efficient for complex data needs
    • May cross aggregate boundaries
    • Good GraphQL optimization
  3. Combined Approach: Repositories for CRUD, Query Services for reads

    • Maintains domain integrity for writes
    • Optimizes for complex read operations
    • Clear separation of concerns

This decision impacts how efficiently your application can retrieve and manage data.

Response Creation Approach​

When implementing Infrastructure components that return data, decide how to package the responses:

  1. State Object Returns (Recommended): Return State objects defined in Common Layer

    • Consistent with the overall communication pattern
    • Clear, predictable return types
    • Allows including metadata along with primary data
  2. Direct Entity Returns: Return domain entities directly

    • Simpler implementation
    • Less transformation overhead
    • Limited ability to include contextual data
  3. Custom Response Objects: Define dedicated response types

    • Fine-tuned to specific needs
    • More types to maintain
    • Less consistency across the application

The recommended approach creates consistency with the overall communication strategy.

Presentation Layer Decisions​

UI Architecture Pattern​

Decide which UI architecture pattern to use within the Presentation Layer:

  1. PrismUI: The recommended hierarchical presenter pattern
    • Purpose-built for Prism Architecture
    • Hierarchical presenter structure
    • Intent-based communication
    • Aligns with overall architecture
  1. MVVM: Model-View-ViewModel pattern

    • Widespread adoption
    • Good framework support
    • Two-way data binding capabilities
  2. TCA/Redux: The Composable Architecture or Redux patterns

    • Strict state management
    • Highly predictable data flow
    • Strong functional programming approach

The choice should align with your team's expertise and the specific needs of your application.

State Management Approach​

Decide how UI state will be managed:

  1. Immutable State Objects: Immutable objects for UI state

    • Predictable state transitions
    • Easier debugging
    • May have performance overhead
  2. Observable Properties: Reactive property observation

    • Dynamic updates
    • Framework-friendly (SwiftUI, Combine)
    • May create less traceable state changes
  3. Combined Approach: Immutable core state with observable wrappers

    • Balances predictability and reactivity
    • Works well with modern UI frameworks
    • More complex implementation

This decision affects how responsive and maintainable your UI will be.

Intent Creation Strategy​

When creating Intent objects in the Presentation Layer, decide on your approach:

  1. Component-Level Creation: UI components create intents directly

    • Immediate intent creation at the source
    • May require forwarding up the hierarchy
    • More distributed intent creation logic
  2. Presenter-Level Creation: Presenters create intents based on UI callbacks

    • More centralized intent creation
    • Clearer separation between UI and intent logic
    • May require more callback methods
  3. Hybrid Creation: Strategic placement based on intent complexity

    • Simple intents created at component level
    • Complex intents created at presenter level
    • Balances distribution and centralization

This decision impacts how user actions are translated into application operations.

Cross-Cutting Decisions​

Dependency Injection Approach​

Decide how dependencies will be managed:

  1. Manual Dependency Injection: Pass dependencies explicitly

    • Simple implementation
    • Clear dependency flow
    • Can be verbose
  2. Service Locator: Central registry of dependencies

    • Simpler component initialization
    • Less explicit dependencies
    • May hide dependencies
  3. Dependency Injection Framework: Use a dedicated DI framework

    • Automates dependency management
    • Can handle complex dependency graphs
    • Adds framework complexity

This decision affects how components are created and composed throughout your application.

Concurrency Model​

Decide which concurrency approach to use:

  1. Swift Concurrency (async/await): Modern structured concurrency

    • Clear asynchronous code flow
    • Strong compiler support
    • Excellent error handling
  2. Combine Framework: Reactive programming approach

    • Stream-based data processing
    • Good for event-driven architecture
    • Steeper learning curve
  3. GCD (Grand Central Dispatch): Traditional queue-based concurrency

    • Fine-grained control
    • Ubiquitous in older code
    • More complex error handling

This decision shapes how asynchronous operations are handled throughout your application.

Testing Strategy​

Decide how your application will be tested:

  1. Layer-Isolated Testing: Focus on testing layers independently

    • Isolates concerns for testing
    • Faster test execution
    • May miss integration issues
  2. Flow-Based Testing: Test complete flows through the architecture

    • Catches integration issues
    • Tests real user scenarios
    • Slower test execution
  3. Combined Testing Strategy: Layer tests for components, flow tests for integration

    • Balances isolation and integration
    • Coverage at multiple levels
    • More comprehensive test suite

This decision affects how thoroughly and efficiently your application can be tested.

Conclusion​

The implementation decisions outlined in this document significantly impact how your Prism Architecture application will be structured, developed, and maintained. While there's no single "correct" set of choices for all projects, understanding these decision points allows you to make informed choices based on your specific requirements.

For communication between layers, the recommended approach is to:

  1. Define all Intent and State types in the Common Layer with a clear folder structure
  2. Create actual objects in the originating layers at runtime
  3. Follow the package delivery model where objects flow through the architecture
  4. Maintain immutability of all communication objects

This approach provides a clean, consistent communication mechanism while maintaining proper layer boundaries and separation of concerns.

When making these decisions, consider:

  • Team size and expertise
  • Project scale and complexity
  • Development timeline and priorities
  • Long-term maintenance needs
  • Platform-specific considerations
  • Performance requirements

Remember that Prism Architecture is designed to be adaptable. You can tailor these decisions to your specific context while still maintaining the core principles that make Prism Architecture effective.