Skip to main content

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​

  1. Explicit Architectural Enforcement: Layer dependencies are enforced at the compilation level through package dependencies
  2. Clear Boundaries: Makes layer responsibilities explicit and prevents unauthorized access across layers
  3. Isolated Testing: Each layer has its own dedicated test target with layer-specific mocks
  4. Compilation Efficiency: Changes in one layer don't force recompilation of the entire codebase
  5. Team Scalability: Different teams can work on different packages with minimal conflict
  6. Reusability: Core and Common packages can be easily reused across projects
  7. Focused Scope: Developers working on a layer only see the code relevant to that layer

Disadvantages​

  1. Setup Complexity: Requires more initial configuration and management of package dependencies
  2. Refactoring Overhead: Moving functionality between layers requires package changes
  3. Build System Complexity: More complex build pipeline and potential for dependency resolution issues
  4. Fragmented Codebase: Can make it harder to get a holistic view of the application
  5. 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​

  1. Simplicity: Easier setup and management with a standard project structure
  2. Flexibility: Easier to refactor and move code between layers
  3. Unified Build Process: Simpler build pipeline with fewer dependency resolution issues
  4. Holistic View: Easier to understand the entire application at once
  5. Lower Overhead: Less configuration and management required
  6. Easier Navigation: Straightforward to navigate between layers in the IDE

Disadvantages​

  1. Limited Architectural Enforcement: Relies on developer discipline and code reviews to maintain layer boundaries
  2. Accidental Dependencies: Easier to accidentally create inappropriate dependencies between layers
  3. Larger Compilation Units: Changes in one layer may trigger larger recompilations
  4. Test Contamination: Potential for tests to access components they shouldn't
  5. 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​

  1. Balance: Good balance between architectural enforcement and development flexibility
  2. Foundation Stability: Core and Common layers get strict package boundaries
  3. Application Flexibility: Application-specific layers have easier refactoring
  4. Reusability: Foundation packages can be shared across projects
  5. Selective Testing Isolation: Foundation layers get isolated tests
  6. Compromise on Complexity: Less complexity than full package approach

Disadvantages​

  1. Partial Enforcement: Only some architectural boundaries are strictly enforced
  2. Mixed Development Model: Developers need to work with both packages and monolithic code
  3. Inconsistent Structure: Potentially confusing to have different approaches for different layers
  4. 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​

  1. Feature Isolation: Complete features are isolated in their own packages
  2. Team Alignment: Teams can own entire features rather than layers
  3. Combined Approach: Maintains layer structure within each feature
  4. Parallel Development: Features can be developed in parallel
  5. Feature Reusability: Features can potentially be reused across applications
  6. Modular Testing: Features can be tested in isolation

Disadvantages​

  1. Potential Duplication: May lead to code duplication across features
  2. Layer Consistency: Harder to ensure consistent implementation across layers
  3. Cross-Feature Dependencies: Managing dependencies between features can be challenging
  4. Structure Complexity: More complex structure to understand and maintain
  5. 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:

  1. Imports: At the top of the file
  2. Types Declaration: Protocols, classes, structs, and enums
  3. Properties: Constants and variables
  4. Initialization: Initializers and setup methods
  5. Public Interface: Public methods and functions
  6. Private Helpers: Private implementation details
  7. 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​

  1. Clear Import Statements: Make dependencies explicit through imports
  2. Access Control: Use access modifiers to restrict visibility when appropriate
  3. Dependency Injection: Pass dependencies explicitly rather than using globals
  4. Layer-Specific Tests: Test each layer in isolation with appropriate mocks

File and Type Organization​

  1. Logical Grouping: Group related files in folders that reflect their purpose
  2. Consistent Naming: Follow naming conventions consistently across the codebase
  3. Type Placement: Each type should be in its own file, unless tightly coupled
  4. Documentation: Include header documentation explaining the purpose of each file

Project Evolution Strategies​

  1. Start Simple: Begin with a simpler structure and evolve as needed
  2. Incremental Modularization: Extract packages one at a time, starting with Core and Common
  3. Refactoring Windows: Schedule dedicated time for structural refactoring
  4. Documentation: Keep documentation of project structure up to date

Common Anti-Patterns to Avoid​

Structural Anti-Patterns​

  1. Layer Violation: Bypassing architectural boundaries for convenience
  2. Wrong Layer Placement: Placing logic in the wrong architectural layer
  3. Circular Dependencies: Creating mutual dependencies between packages
  4. Structure Inconsistency: Using different organizational patterns across the project
  5. Monolithic Packages: Packages that are too large and contain unrelated functionality

Organizational Anti-Patterns​

  1. Feature/Layer Mismatch: Project structure that doesn't align with team organization
  2. Overengineering: Complex structure for simple applications
  3. Premature Modularization: Breaking into packages before boundaries are clear
  4. 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.