4.6 KiB
ADR-003: Feature-Based Package Organization
Status: Accepted
Date: 2025-01-11
Context:
With a modular architecture established via Swift Package Manager, I needed to define how to organize code within each package. The key questions were:
- What constitutes a "feature" worthy of its own package?
- How should layers be represented within a feature package?
- What should be public vs. private in each package?
- How do features integrate with the main application?
Decision:
I adopted a feature-based package organization where each domain feature (Auth, Status, Account, PURLs, Pastebin, etc.) is a separate package with multiple internal targets representing architectural layers.
Package Structure Pattern:
FeatureName/
├── Package.swift # Dependencies and targets
├── Sources/
│ ├── FeatureName/ # Main UI and app integration
│ │ ├── Views/ # All SwiftUI views
│ │ │ ├── App/ # Main feature view and view model
│ │ │ └── [Feature Areas]/ # Other views grouped by functionality
│ │ ├── Scenes/ # Private window scenes (WindowGroup)
│ │ ├── Factories/ # AppFactory with public integration methods
│ │ └── Environment/ # Feature environment and DI setup
│ ├── FeatureNameService/ # Business logic layer (optional)
│ ├── FeatureNameRepository/ # Data coordination
│ ├── FeatureNameNetworkService/ # API communication
│ └── FeatureNamePersistenceService/ # Swift Data storage
└── Tests/ # Unit tests for each layer
Public API Surface:
Each feature package exposes a minimal public API through its AppFactory:
makeAppView()- Creates the main feature view for display in the sidebarmakeScene()- Returns window scenes (private to the module) assome ScenemakeSettingsView()- Creates settings views (when applicable)
Everything else (scenes, view models, repositories, services) remains internal to the package.
Feature Environment:
Each feature has its own Environment struct that:
- Manages a
MicroContainerfor dependency resolution - Registers all layer factories (NetworkService, PersistenceService, Repository)
- Exposes
viewModelFactoryandmodelContainerproperties - Is passed to scenes which use it to build views
Feature Categories:
- Domain Features: Auth, Account, Status, Now, PURLs, Webpage, Pastebin, Weblog, Pics
- Infrastructure Packages: OMGAPI, DesignSystem, SessionService, AuthSession, Route, Sidebar
- Utility Packages: FoundationExtensions, Utilities
Integration with Main App:
The main application (TritonApp) uses TritonEnvironment to resolve all feature factories via MicroContainer. Scenes are now private within feature modules and receive the feature Environment to build views.
Consequences:
Positive
- Feature isolation: Each feature is self-contained with clear boundaries
- Parallel development: Features can evolve independently without conflicts
- Selective testing: Can test individual features without loading others
- Clear ownership: Each feature package owns its complete vertical slice
- Reduced coupling: Features cannot access each other's internals
- Minimal public API: Only integration points are exposed, reducing breaking changes
Negative
- Package proliferation: Many packages to navigate (mitigated by clear naming)
- Cross-feature sharing: Shared functionality must be extracted to infrastructure packages
- Discovery: Finding code requires knowing which feature it belongs to
Neutral
- Feature granularity: Judgment calls about feature boundaries (e.g., should PURLs and Status be one package?)
- Shared UI components: Decision about when to move components to DesignSystem vs. keeping them in features
Related Decisions:
- ADR-001: Modular Architecture with Swift Package Manager - Enables package-based organization
- ADR-002: Layered Architecture and Dependency Direction - Layers within packages
- ADR-010: AppFactory Pattern for Feature Integration - How features expose themselves
Notes:
This structure was designed from the start to support clean feature boundaries and vertical slice architecture. Each package represents a complete feature with all its layers.