Pular para conteúdo

ADR-0001: Monolithic Modular Architecture

Status

Accepted

Context

We needed to choose an architecture for the Spryx Backend that would:

  1. Support rapid development - Small team, need to move fast
  2. Allow future scaling - May need to extract services later
  3. Maintain code quality - Clear boundaries and testability
  4. Enable team growth - Easy for new developers to understand

The main options were: - Microservices from the start - Traditional monolith - Monolithic modular architecture

Decision

We chose Monolithic Modular Architecture with Clean Architecture principles.

This means: - Single deployable application - Code organized into well-defined modules - Modules communicate through defined interfaces - Each module could be extracted to a service if needed

Module Structure

src/app/modules/
├── rag/              # RAG module
├── billing/          # Billing module (future)
└── agent/            # Agent module (future)

Key Principles

  1. Modules are bounded contexts - Each module has its own domain models
  2. Clean Architecture layers - Domain → Application → Infrastructure
  3. Dependency inversion - Application depends on ports, not implementations
  4. Vertical slices - Features organized by domain, not layer

Consequences

Positive

  1. Simpler deployment
  2. Single application to deploy
  3. No service discovery needed
  4. Easier debugging and tracing

  5. Faster development

  6. No network overhead between modules
  7. Refactoring is a code change, not distributed system change
  8. IDE support for cross-module navigation

  9. Lower operational complexity

  10. One database (with logical separation)
  11. One deployment pipeline
  12. One monitoring setup

  13. Future extraction possible

  14. Module boundaries = potential service boundaries
  15. Ports enable swapping implementations
  16. Communication patterns already defined

Negative

  1. Scaling constraints
  2. All modules scale together
  3. Can't independently scale hot modules

  4. Technology lock-in

  5. All modules use Python
  6. Can't use best-fit language per module

  7. Single point of failure

  8. One app down = everything down
  9. Requires robust error handling

  10. Discipline required

  11. Module boundaries must be enforced manually
  12. Easy to accidentally cross boundaries

Neutral

  1. Testing approach
  2. Can use unit tests for domain/application
  3. Integration tests need single database
  4. No distributed system testing complexity

  5. Database strategy

  6. Shared database with separate tables per module
  7. Could migrate to separate schemas/databases later

Alternatives Considered

Microservices from Start

Why not chosen: - Premature optimization - Too much operational complexity for small team - Network overhead for every inter-service call - Harder to refactor domain boundaries - Distributed transactions complexity

Traditional Monolith (no module structure)

Why not chosen: - No clear boundaries - Hard to maintain as codebase grows - Difficult to extract later - Coupling between features

Serverless Functions

Why not chosen: - Cold start latency issues - Complex state management - Vendor lock-in - Debugging difficulty

Implementation Notes

Module Communication

Within the monolith, modules communicate through: - Ports (interfaces) for synchronous operations - Shared types for IDs and common exceptions

# Module A defines a port
class DocumentLookupPort(Protocol):
    async def get_document_title(self, doc_id: str) -> str | None: ...

# Module B implements it
class DocumentLookupAdapter:
    async def get_document_title(self, doc_id: str) -> str | None:
        doc = await self.doc_repo.get_by_id(doc_id)
        return doc.title if doc else None

Future Extraction

When ready to extract a module: 1. Replace in-process port calls with HTTP/gRPC clients 2. Split database tables to separate database 3. Deploy module as separate service 4. Update service discovery

Enforcement

Module boundaries are enforced through: - Code review (checklist) - Import conventions (absolute paths) - Directory structure - Documentation

References