Documentation

adrs/001-polylith-architecture-adoption.md

ADR-001: Adoption of Polylith Architecture for .NET

Status

Accepted

Context

The Acsis asset tracking system had grown into a large monolithic application that was becoming increasingly difficult to maintain, test, and deploy. Key challenges included:

  • Tight coupling: Components were heavily interdependent, making changes risky
  • Monolithic deployment: Any change required deploying the entire application
  • Development bottlenecks: Teams couldn't work independently on different features
  • Testing complexity: Integration tests were slow and brittle
  • Technology constraints: Difficult to adopt new technologies or frameworks incrementally

Traditional microservices architecture was considered but presented challenges:

  • Network complexity: Service discovery, network latency, distributed transactions
  • Operational overhead: Managing multiple deployments, monitoring, logging
  • Development complexity: Local development environment setup
  • Data consistency: Managing distributed data and eventual consistency

Decision

We decided to adopt the Polylith architecture pattern, adapting it from its original Clojure implementation to work with the .NET ecosystem.

Key architectural decisions:

  1. Monorepo structure: Single repository containing all components, bases, and projects
  2. Component isolation: Each component has clear interfaces and implementations
  3. Runtime composition: Components are loaded dynamically at application startup
  4. Shared development workspace: All components can be developed and tested together
  5. Flexible deployment: Components can be composed into different applications as needed

The adapted Polylith structure includes:

  • Components: Reusable business logic with clear interfaces
  • Bases: Application entry points (ASP.NET Core APIs, background services, etc.)
  • Projects: Deployment artifacts that combine bases with selected components
  • Development: Shared workspace containing all components and bases

Consequences

Positive

Development Benefits:

  • Loose coupling: Components only depend on interfaces, never implementations
  • Independent development: Teams can work on components without interfering
  • Shared codebase: All code in one repository, easier to coordinate changes
  • Consistent tooling: Same build system, testing framework, and deployment process
  • Simplified testing: Components can be tested in isolation or together

Operational Benefits:

  • Flexible deployment: Can deploy different combinations of components
  • Gradual migration: Can migrate components incrementally
  • Single deployment unit: Still benefits from monolithic deployment simplicity
  • Technology evolution: Can adopt new frameworks component by component

Architectural Benefits:

  • Clear boundaries: Component interfaces make dependencies explicit
  • Reusability: Components can be used in multiple applications
  • Testability: Each component can be unit tested independently
  • Modularity: System is naturally organized into logical modules

Negative

Complexity Challenges:

  • Learning curve: Team needs to understand Polylith concepts and .NET adaptations
  • Custom tooling: Required custom MSBuild targets and component loading logic
  • Runtime dependencies: Component loading failures only discovered at runtime
  • Architecture discipline: Requires strict adherence to interface/implementation separation

Technical Challenges:

  • Assembly loading: Complex dynamic loading with potential version conflicts
  • Debugging complexity: Runtime component loading can complicate debugging
  • Build system complexity: More complex MSBuild configuration than traditional solutions
  • Non-standard approach: Deviates from typical .NET project structures

Operational Considerations:

  • Component versioning: No built-in solution for component version management
  • Deployment coordination: Still requires coordinating deployments across teams
  • Performance overhead: Runtime component loading adds startup time
  • Monitoring complexity: Need to track component loading and health

Mitigation Strategies

To address the negative consequences:

  1. Roslyn Analyzers: Custom analyzers enforce architectural rules at compile time
  2. Comprehensive documentation: Detailed guides for developers and operators
  3. Component loader logging: Detailed logging for component loading troubleshooting
  4. Development tooling: IDE support through .slnx solution format
  5. Gradual adoption: Migrate existing system incrementally rather than big-bang rewrite
  • ADR-002: Use .Spec suffix for interface projects
  • ADR-003: Dynamic component loading implementation
  • ADR-004: Architectural rule enforcement

References