Skip to main content

ADR-0008: Modular, Event-Driven Service Architecture

Status: Accepted Date: 2025-12-05

Context

As the platform has matured, initial services like EvaluationService and ResultsService have grown to encompass multiple, distinct domains (e.g., authoring, buckets, delivery, remediation). This trend leads to a "god service" anti-pattern, which increases cognitive load, complicates testing, and makes future extensions more difficult.

In contrast, newer features like "Programmes" have successfully used a more decoupled, event-driven pattern (ProgrammeOrchestrator, ProgrammeProgressListener), proving its effectiveness for creating maintainable, cross-feature interactions.

To ensure long-term architectural health and developer velocity, we need to formalize this successful pattern as a guiding principle for both refactoring existing code and developing new features.

Decision

We will officially adopt and enforce a "Modular Monolith" architecture guided by the following principles:

  1. Favor Single-Responsibility Services: Large, multi-domain services will be progressively refactored into smaller, more focused services with clear boundaries. For example, EvaluationService should be treated as an orchestrator, with its underlying logic broken out into dedicated services like a BucketService or SectionService. Similarly, the Remediation logic should be extracted from ResultsService.

  2. Prioritize Event-Driven Communication: For interactions that cross feature boundaries, we will use a decoupled, event-driven "listener" pattern. Primary services (e.g., ResultsService) should emit domain events (e.g., "submission created"), and secondary services (e.g., ProgrammeProgressListener) will listen and react to them. This avoids tight coupling and makes the system more resilient to change.

  3. No Networked Microservices: The term "service" in this context refers to a Go package within the same application process (e.g., internal/services/remediation). All services will continue to operate within the same monolith, using the shared TxManager and RLS infrastructure. We will not introduce network-level microservices at this stage.

Consequences

Positive

  • Reduced Cognitive Load: Developers can work within smaller, more focused domains, making the codebase easier to understand and modify.
  • Improved Testability: Smaller services with clear interfaces are significantly easier to unit test in isolation.
  • Clear Ownership: Business domains (like Remediation, Programmes, Buckets) will have clear owners, improving accountability and consistency.
  • Architectural Coherence: Establishes a consistent, scalable pattern for adding new features, ensuring they integrate as "first-class citizens."

Negative

  • Requires Refactoring Effort: This decision requires a deliberate, ongoing effort to refactor existing large services.
  • Increased Package Count: Results in a greater number of service packages to manage within the project structure.

This decision codifies the best practices already emerging in the codebase and provides a clear strategic direction for maintaining a healthy, scalable, and modular monolith.