Project Structure
Reading time: ~15 minutes
Introductionβ
How you organize the physical structure of your Prism Architecture project has a significant impact on development workflow, team collaboration, maintainability, and architectural enforcement. While the conceptual layers of Prism Architecture define a clear separation of concerns, you must decide how these layers are arranged in your codebase.
This document explores different approaches to structuring a Prism Architecture project, from strict package-based implementations to monolithic approaches and hybrid solutions. Understanding these options will help you select the most appropriate structure for your specific project requirements and team dynamics.
Key Considerations for Project Structureβ
Before exploring specific structural approaches, consider these key factors that should influence your decision:
Architectural Enforcementβ
How strictly do you want to enforce architectural boundaries? Different project structures provide varying levels of enforcement:
- Compiler-Enforced: Boundaries enforced by the compiler through module/package dependencies
- Convention-Based: Boundaries maintained through team discipline and code reviews
- Hybrid Approaches: Critical boundaries enforced in compilation, others by convention
Team Organizationβ
Your team's size, structure, and working patterns should influence your project structure:
- Team Size: Larger teams often benefit from more explicit boundaries
- Team Organization: Feature teams vs. platform teams have different structural needs
- Development Experience: More experienced teams may need less structural enforcement
Reusability Requirementsβ
Consider which parts of your codebase might be reused:
- Cross-Project Reuse: Core and Common layers often benefit from being separate modules
- Component Reusability: Feature modules may be reused across different apps
- Internal Reuse: How easily developers can find and reuse existing components
Development Efficiencyβ
The impact of structure on daily development tasks:
- Build Times: More modular structures can improve incremental build times
- Navigation and Discovery: How easily developers can find relevant code
- Refactoring Overhead: Cost of moving code between architectural boundaries
Approach 1: Swift Package Per Layerβ
This approach implements each architectural layer as a separate Swift Package, creating strong boundaries between layers.
Structureβ
PrismProject/
βββ CorePackage/ # Core Layer
β βββ Sources/
β βββ Tests/ # Tests + Mocks for Core components
βββ CommonPackage/ # Common Layer
β βββ Sources/
β βββ Tests/ # Tests + Mocks for Common components
βββ DomainPackage/ # Domain Layer
β βββ Sources/
β βββ Tests/ # Tests + Mocks for Domain components
β βββ Package.swift # Depends on CorePackage
βββ InfrastructurePackage/ # Infrastructure Layer
β βββ Sources/
β βββ Tests/ # Tests + Mocks for Infrastructure
β βββ Package.swift # Depends on CorePackage, CommonPackage
βββ OrchestrationPackage/ # Orchestration Layer
β βββ Sources/
β βββ Tests/ # Tests + Mocks for Orchestration
β βββ Package.swift # Depends on Core, Domain, Infrastructure
βββ PresentationPackage/ # Presentation Layer
β βββ Sources/
β βββ Tests/ # Tests + Mocks for Presentation
β βββ Package.swift # Depends on Core, Common, Orchestration
βββ App/ # Main Application
βββ Sources/
βββ Package.swift # Depends on all other packages
Advantagesβ
- Explicit Architectural Enforcement: Layer dependencies are enforced at the compilation level through package dependencies
- Clear Boundaries: Makes layer responsibilities explicit and prevents unauthorized access across layers
- Isolated Testing: Each layer has its own dedicated test target with layer-specific mocks
- Compilation Efficiency: Changes in one layer don't force recompilation of the entire codebase
- Team Scalability: Different teams can work on different packages with minimal conflict
- Reusability: Core and Common packages can be easily reused across projects
- Focused Scope: Developers working on a layer only see the code relevant to that layer
Disadvantagesβ
- Setup Complexity: Requires more initial configuration and management of package dependencies
- Refactoring Overhead: Moving functionality between layers requires package changes
- Build System Complexity: More complex build pipeline and potential for dependency resolution issues
- Fragmented Codebase: Can make it harder to get a holistic view of the application
- Overhead for Small Teams: May introduce unnecessary complexity for smaller projects
When to Useβ
- Large enterprise applications
- Projects with multiple teams working in parallel
- Codebases where architectural integrity is critical
- Projects where layers may be reused across multiple applications
- Teams with strong Swift Package expertise
Approach 2: Monolithic Project Structureβ
This approach maintains all architectural layers within a single project structure, relying on folder organization and developer discipline.
Structureβ
PrismProject/
βββ Sources/
β βββ Core/ # Core Layer
β βββ Common/ # Common Layer
β βββ Domain/ # Domain Layer
β βββ Infrastructure/ # Infrastructure Layer
β βββ Orchestration/ # Orchestration Layer
β βββ Presentation/ # Presentation Layer
βββ Tests/
βββ CoreTests/
βββ CommonTests/
βββ DomainTests/
βββ InfrastructureTests/
βββ OrchestrationTests/
βββ PresentationTests/
Advantagesβ
- Simplicity: Easier setup and management with a standard project structure
- Flexibility: Easier to refactor and move code between layers
- Unified Build Process: Simpler build pipeline with fewer dependency resolution issues
- Holistic View: Easier to understand the entire application at once
- Lower Overhead: Less configuration and management required
- Easier Navigation: Straightforward to navigate between layers in the IDE
Disadvantagesβ
- Limited Architectural Enforcement: Relies on developer discipline and code reviews to maintain layer boundaries
- Accidental Dependencies: Easier to accidentally create inappropriate dependencies between layers
- Larger Compilation Units: Changes in one layer may trigger larger recompilations
- Test Contamination: Potential for tests to access components they shouldn't
- Team Conflicts: More potential for merge conflicts when multiple developers work on the same layer
When to Useβ
- Smaller applications
- Projects with a small development team (1-3 developers)
- Prototypes and MVPs where speed of development is prioritized
- Projects where the architecture is still evolving
- Teams new to Prism Architecture who want to focus on concepts before structure
Approach 3: Hybrid - Foundation Packagesβ
This approach extracts only the Core and Common layers as separate packages, keeping application-specific layers in a monolithic structure.
Structureβ
PrismProject/
βββ CorePackage/ # Core Layer as a package
β βββ Sources/
β βββ Tests/
βββ CommonPackage/ # Common Layer as a package
β βββ Sources/
β βββ Tests/
βββ MainApp/ # Main application (monolithic)
βββ Sources/
β βββ Domain/ # Domain Layer
β βββ Infrastructure/
β βββ Orchestration/
β βββ Presentation/
βββ Tests/
βββ DomainTests/
βββ InfrastructureTests/
βββ ...
Advantagesβ
- Balance: Good balance between architectural enforcement and development flexibility
- Foundation Stability: Core and Common layers get strict package boundaries
- Application Flexibility: Application-specific layers have easier refactoring
- Reusability: Foundation packages can be shared across projects
- Selective Testing Isolation: Foundation layers get isolated tests
- Compromise on Complexity: Less complexity than full package approach
Disadvantagesβ
- Partial Enforcement: Only some architectural boundaries are strictly enforced
- Mixed Development Model: Developers need to work with both packages and monolithic code
- Inconsistent Structure: Potentially confusing to have different approaches for different layers
- Still Has Package Overhead: Still requires management of some packages
When to Useβ
- Medium-sized applications
- Teams transitioning to a package-based approach
- Projects where Core and Common layers are stable but other layers evolve frequently
- Multiple projects that share the same foundation but have different implementations
- Teams that want some architectural enforcement without full package complexity
Approach 4: Hybrid - Feature-Based Packagesβ
This approach organizes code into feature-based packages that contain vertical slices across all layers.
Structureβ
PrismProject/
βββ CorePackage/ # Core Layer - shared
βββ CommonPackage/ # Common Layer - shared
βββ FeatureAPackage/ # Complete feature vertical slice
β βββ Sources/
β β βββ Domain/ # Feature A domain
β β βββ Infrastructure/ # Feature A infrastructure
β β βββ Orchestration/ # Feature A orchestration
β β βββ Presentation/ # Feature A presentation
β βββ Tests/
βββ FeatureBPackage/ # Another feature vertical slice
βββ MainApp/ # App entry point and shared resources
Advantagesβ
- Feature Isolation: Complete features are isolated in their own packages
- Team Alignment: Teams can own entire features rather than layers
- Combined Approach: Maintains layer structure within each feature
- Parallel Development: Features can be developed in parallel
- Feature Reusability: Features can potentially be reused across applications
- Modular Testing: Features can be tested in isolation
Disadvantagesβ
- Potential Duplication: May lead to code duplication across features
- Layer Consistency: Harder to ensure consistent implementation across layers
- Cross-Feature Dependencies: Managing dependencies between features can be challenging
- Structure Complexity: More complex structure to understand and maintain
- Layer Visibility: Layer structure may become secondary to feature structure
When to Useβ
- Large applications with distinct features
- Projects with feature-based teams
- Applications where features are more independent than interrelated
- Projects where features may be enabled/disabled independently
- Organizations where team structure aligns with features rather than architecture layers
Layer-Specific Organizationβ
Beyond the overall project structure, each layer should have its own internal organization that reflects its specific concerns.
Core Layer Organizationβ
Core/
βββ Entities/ # Domain entities
β βββ BaseEntityProtocol.swift
β βββ User/
β β βββ User.swift
β βββ Product/
β β βββ Product.swift
β βββ ...
βββ ValueObjects/ # Value objects
β βββ BaseValueObjectProtocol.swift
β βββ Money.swift
β βββ EmailAddress.swift
β βββ ...
βββ Enumerations/ # Domain-specific enumerations
β βββ ProductCategory.swift
β βββ OrderStatus.swift
β βββ ...
βββ Protocols/ # Core protocols
βββ Repositories/
β βββ UserRepositoryProtocol.swift
β βββ ProductRepositoryProtocol.swift
β βββ ...
βββ Services/
βββ AuthenticationServiceProtocol.swift
βββ ...
Domain Layer Organizationβ
Domain/
βββ Services/ # Domain services
β βββ UserService.swift
β βββ ProductService.swift
β βββ ...
βββ Validation/ # Validation services
β βββ UserValidationService.swift
β βββ ProductValidationService.swift
β βββ ...
βββ Rules/ # Business rules
β βββ PricingRules.swift
β βββ DiscountRules.swift
β βββ ...
βββ Events/ # Domain events
βββ UserEvents.swift
βββ OrderEvents.swift
βββ ...
Orchestration Layer Organizationβ
Orchestration/
βββ UseCases/ # Entity-focused use cases
β βββ User/
β β βββ RegisterUserUseCase.swift
β β βββ UpdateUserProfileUseCase.swift
β β βββ ...
β βββ Product/
β β βββ CreateProductUseCase.swift
β β βββ UpdateProductUseCase.swift
β β βββ ...
β βββ ...
βββ Services/ # Orchestration services
β βββ AuthenticationOrcService.swift
β βββ CatalogOrcService.swift
β βββ ...
βββ Workflows/ # Complex workflows
β βββ CheckoutWorkflow.swift
β βββ OnboardingWorkflow.swift
β βββ ...
βββ EventHandlers/ # Domain event handlers
βββ UserEventHandlers.swift
βββ OrderEventHandlers.swift
βββ ...
Infrastructure Layer Organizationβ
Infrastructure/
βββ DataAccess/ # Data access components
β βββ Repositories/ # Repository implementations
β β βββ UserRepository.swift
β β βββ ProductRepository.swift
β β βββ ...
β βββ QueryServices/ # Specialized query services
β β βββ UserQueryService.swift
β β βββ ProductQueryService.swift
β β βββ ...
β βββ REST/ # REST API components
β β βββ RestClient.swift
β β βββ Endpoints/
β β βββ ...
β βββ GraphQL/ # GraphQL components
β βββ GraphQLClient.swift
β βββ Operations/
β βββ ...
βββ SystemServices/ # System service integrations
β βββ NotificationService.swift
β βββ LocationService.swift
β βββ ...
βββ ExternalServices/ # External service integrations
βββ PaymentGatewayService.swift
βββ AnalyticsService.swift
βββ ...
Presentation Layer Organizationβ
Presentation/
βββ Features/ # Feature-specific UI
β βββ Auth/ # Authentication feature
β β βββ Login/
β β β βββ LoginView.swift
β β β βββ LoginPresenter.swift
β β β βββ LoginState.swift
β β β βββ LoginIntent.swift
β β βββ Register/
β β βββ ...
β βββ Catalog/ # Product catalog feature
β β βββ List/
β β βββ Detail/
β β βββ ...
β βββ ...
βββ Components/ # Shared UI components
β βββ Buttons/
β βββ Cards/
β βββ Lists/
β βββ ...
βββ Navigation/ # Navigation components
β βββ Router.swift
β βββ NavigationCoordinator.swift
β βββ ...
βββ Utils/ # UI utilities
βββ FormatterUtils.swift
βββ UIExtensions.swift
βββ ...
Within-File Organizationβ
Beyond folder structure, consistent organization within files is important for maintainability:
Protocol-First Patternβ
Place protocols before their implementations:
// Core/Protocols/Repositories/ProductRepositoryProtocol.swift
public protocol ProductRepositoryProtocol {
func getProduct(id: ProductID) async throws -> Product?
func getProductList() async throws -> [Product]
func saveProduct(_ product: Product) async throws
}
// Infrastructure/DataAccess/Repositories/ProductRepository.swift
public final class ProductRepository: ProductRepositoryProtocol {
// Implementation details...
}
Standard File Structureβ
For consistency, each file should follow a standard structure:
- Imports: At the top of the file
- Types Declaration: Protocols, classes, structs, and enums
- Properties: Constants and variables
- Initialization: Initializers and setup methods
- Public Interface: Public methods and functions
- Private Helpers: Private implementation details
- Extensions: Extensions of the primary type
File Naming Conventionsβ
Consistent file naming enhances discoverability and clarity:
- Entities:
[EntityName].swift
(e.g.,Product.swift
) - Value Objects:
[ValueObjectName].swift
(e.g.,Money.swift
) - Protocols:
[Name]Protocol.swift
(e.g.,ProductRepositoryProtocol.swift
) - Use Cases:
[Operation][EntityName]UseCase.swift
(e.g.,CreateProductUseCase.swift
) - Services:
[Domain][Service].swift
(e.g.,ProductPriceService.swift
) - Orchestration Services:
[Feature]OrcService.swift
(e.g.,CatalogOrcService.swift
) - Repositories:
[EntityName]Repository.swift
(e.g.,ProductRepository.swift
) - Presenters:
[Feature]Presenter.swift
(e.g.,ProductListPresenter.swift
) - Views:
[Feature]View.swift
(e.g.,ProductListView.swift
) - States:
[Feature]State.swift
(e.g.,ProductListState.swift
) - Intents:
[Feature]Intent.swift
(e.g.,ProductListIntent.swift
)
Test Organizationβ
Test organization should mirror your production code structure while adding test-specific concerns:
Package-Per-Layer Test Structureβ
CorePackage/
βββ Sources/
β βββ ...
βββ Tests/
βββ Entities/
β βββ ProductTests.swift
β βββ ...
βββ ValueObjects/
β βββ MoneyTests.swift
β βββ ...
βββ Mocks/
βββ MockProductRepository.swift
βββ ...
Monolithic Test Structureβ
PrismProject/
βββ Sources/
β βββ ...
βββ Tests/
βββ CoreTests/
β βββ Entities/
β βββ ValueObjects/
β βββ Mocks/
βββ DomainTests/
β βββ Services/
β βββ Validation/
β βββ Mocks/
βββ ...
Decision Factorsβ
When choosing a project structure for Prism Architecture, consider these factors:
Team Size and Organizationβ
- Small Teams (1-3): Consider monolithic or hybrid approach
- Medium Teams (4-8): Consider hybrid approaches
- Large Teams (9+): Consider package-per-layer or feature-based packages
Project Size and Complexityβ
- Small Projects: Monolithic structure is usually sufficient
- Medium Projects: Hybrid approaches offer good balance
- Large Projects: Full package separation provides necessary boundaries
Architectural Enforcement Priorityβ
- High Priority: Package-per-layer approach
- Medium Priority: Hybrid with foundation packages
- Lower Priority: Monolithic structure with good conventions
Development Velocity Requirementsβ
- Speed-Focused: Monolithic or minimal hybrid
- Balance: Hybrid approaches
- Structure-Focused: Full package separation
Reusability Needsβ
- High Reusability: Package-per-layer or feature-based packages
- Selective Reusability: Hybrid with foundation packages
- Application-Specific: Monolithic structure
Implementation Guidelinesβ
Regardless of the project structure chosen, follow these guidelines for successful implementation:
Layer Boundary Enforcementβ
- Clear Import Statements: Make dependencies explicit through imports
- Access Control: Use access modifiers to restrict visibility when appropriate
- Dependency Injection: Pass dependencies explicitly rather than using globals
- Layer-Specific Tests: Test each layer in isolation with appropriate mocks
File and Type Organizationβ
- Logical Grouping: Group related files in folders that reflect their purpose
- Consistent Naming: Follow naming conventions consistently across the codebase
- Type Placement: Each type should be in its own file, unless tightly coupled
- Documentation: Include header documentation explaining the purpose of each file
Project Evolution Strategiesβ
- Start Simple: Begin with a simpler structure and evolve as needed
- Incremental Modularization: Extract packages one at a time, starting with Core and Common
- Refactoring Windows: Schedule dedicated time for structural refactoring
- Documentation: Keep documentation of project structure up to date
Common Anti-Patterns to Avoidβ
Structural Anti-Patternsβ
- Layer Violation: Bypassing architectural boundaries for convenience
- Wrong Layer Placement: Placing logic in the wrong architectural layer
- Circular Dependencies: Creating mutual dependencies between packages
- Structure Inconsistency: Using different organizational patterns across the project
- Monolithic Packages: Packages that are too large and contain unrelated functionality
Organizational Anti-Patternsβ
- Feature/Layer Mismatch: Project structure that doesn't align with team organization
- Overengineering: Complex structure for simple applications
- Premature Modularization: Breaking into packages before boundaries are clear
- Inconsistent Conventions: Mixing naming and organizational patterns
Conclusionβ
The physical structure of your Prism Architecture project plays a vital role in its long-term maintainability and development efficiency. The optimal structure depends on your specific context, including team size, project complexity, and organizational needs.
For most projects, a hybrid approach offers a good balanceβusing packages for stable foundational layers (Core and Common) while keeping application-specific layers in a more flexible monolithic structure. As projects grow in size and complexity, moving toward a more package-based approach often makes sense.
The most important factor is consistencyβwhatever approach is chosen should be consistently applied and clearly documented for the team. Remember that project structure should support your architectural goals, not hinder development or create unnecessary complexity.