Action Plan: Refactoring to a Modular Service Architecture
Objective: To progressively refactor the backend to align with the principles of a modular, event-driven architecture as defined in ADR-0008.
This document outlines a phased approach to breaking down large, multi-domain services into smaller, single-responsibility services. This will improve maintainability, testability, and developer velocity.
Phase 1: Extract the Remediation Service
Status: Done
Goal: Decouple the post-submission remediation logic from the core delivery pipeline by moving it from ResultsService into a new, dedicated RemediationService.
Implementation Plan
-
Create New Service Package:
- Create a new directory:
backend/internal/services/remediation/. - Create a new file within it:
remediation_service.go.
- Create a new directory:
-
Define the
RemediationService:- In
remediation_service.go, define theremediation.Servicestruct with a dependency onpg.TxManager. - Create a
NewServiceconstructor.
- In
-
Move Remediation Logic:
- Carefully move all public and private functions related to remediation from
results_service.goto the newremediation_service.go. - This includes
CreateCorrectionBatch,ApplyCorrectionBatch,RevertCorrectionBatch, and their related helpers. - Update the function receivers from
(s *ResultsService)to(s *Service).
- Carefully move all public and private functions related to remediation from
-
Rewire the HTTP Handler:
- Modify
backend/internal/http/handler/results_remediation.goto depend on and use the newremediation.Serviceinstead ofResultsService.
- Modify
-
Update Dependencies:
- In
backend/cmd/server/main.go, instantiate the newremediation.Serviceand inject it into theNewResultsRemediationHandler(done).
- In
-
Update Event Listeners:
- The
RemediationServicemust trigger an event (e.g., "CorrectionBatchApplied") after a batch is successfully applied. - The
ProgrammeProgressListener'sOnCorrectionBatchAppliedhook must be wired to consume this event. - This communication MUST be decoupled. The
RemediationServiceshould not have a direct, compile-time dependency on theProgrammeProgressListener. An in-memory event dispatcher or a similar pattern should be used to adhere to the principles in ADR-0008.
- The
-
Add Focused Tests:
- Created
remediation_service_test.gowith idempotent apply coverage and listener behavior.
- Created
-
Verify and Cleanup:
go test ./...green; remediation logic removed fromresults_service.go.
Phase 2: Modularize the Evaluation Service
Status: Done
Goal: Decompose the large EvaluationService into smaller, single-responsibility services to reduce its complexity and improve maintainability.
Implementation Plan
-
Create New Service Packages:
- Create
backend/internal/services/buckets/. - Create
backend/internal/services/sections/.
- Create
-
Define New Services:
- Created
BucketServiceinbackend/internal/services/buckets/andSectionServiceinbackend/internal/services/sections/.
- Created
-
Move Logic:
- Migrated bucket CRUD/preview/resolution to
buckets.Service; section and section-item CRUD/ordering tosections.Service.
- Migrated bucket CRUD/preview/resolution to
-
Refactor
EvaluationService:- Slimmed to evaluation CRUD/version lifecycle plus preview/validation; delegates bucket resolution via
BucketService. - Internals split into focused files (
evaluation_service.go,evaluation_usage.go,evaluation_preview.go,evaluation_validation.go).
- Slimmed to evaluation CRUD/version lifecycle plus preview/validation; delegates bucket resolution via
-
Rewire Handlers & Dependencies:
evaluation.gonow callsBucketService/SectionServicedirectly for buckets/sections/items.cmd/server/main.goinstantiates and injects bucket/section services alongsideEvaluationService.
-
Add Focused Tests:
- Existing integration coverage stays green; targeted unit tests remain to be added for buckets/sections if needed.
Phase 3: Establish the Scoring/Outcomes Boundary
Status: In Progress (Conceptual; concurrency guardrails added)
Goal: Create a clear conceptual and structural home for all logic related to scoring, outcomes, and progress, which is currently spread across multiple services.
Implementation Plan (Initial Shaping)
-
Create New Package:
- Create a new directory:
backend/internal/services/outcomes/.
- Create a new directory:
-
Define Core Interfaces:
- Within this package, define Go
interfacetypes for the core concepts, such asScoringEngineorProgressTracker. These interfaces will define the "what" (e.g.,CalculateScore,UpdateProgress) without containing the implementation. - ✅ Preview/validation already narrowed behind
services.EvaluationPreviewer; HTTP handlers now depend on this interface instead of the fullEvaluationService, keeping the preview contract isolated for future swap-out. - ✅ Initial outcomes interfaces scaffolded in
internal/services/outcomes/interfaces.go(scoring + progress), ready to be implemented without touching handlers yet. - ✅ Concurrency guardrails in place while we continue the split: remediation and reaper concurrency tests added to catch deadlocks/regressions during further refactors.
- Within this package, define Go
-
Create Scaffolding:
- Create empty Go files like
scoring_engine.gowithin the new package to house future logic.
- Create empty Go files like
-
Create Follow-up Tickets:
- Document the plan to gradually migrate the relevant scoring and progress logic from
ResultsService,RemediationService, andProgrammeProgressListenerinto this new package over time. This avoids a "big bang" refactor while still establishing the clear architectural boundary for future work.
- Document the plan to gradually migrate the relevant scoring and progress logic from
Current Module Boundaries (Conceptual Map)
| Surface | Handler Dependency | Service Interface / Package | Notes |
|---|---|---|---|
| Evaluations CRUD/versions | internal/http/handler/evaluation.go | services.EvaluationService | Core CRUD + version lifecycle. |
| Evaluations preview/validate | internal/http/handler/evaluation.go | services.EvaluationPreviewer (impl: EvaluationService) | Interface boundary set; swap-friendly. |
| Buckets CRUD/preview | internal/http/handler/evaluation.go (BucketRoutes) | buckets.Service | First-class service; no EvaluationService coupling. |
| Sections/items CRUD/order | internal/http/handler/evaluation.go | sections.Service | First-class service; reposition logic unit-tested. |
| Delivery sessions/results | internal/http/handler/evaluation.go (sessions) | services.ResultsService | Now lock-orders assignment/submission rows on session create. |
| Remediation (apply/revert) | internal/http/handler/results_remediation.go | remediation.Service | Idempotent apply with listener hooks. |
| Outcomes (planned) | n/a (to be wired) | outcomes.ScoringEngine, outcomes.ProgressTracker | Interfaces added; implementations to follow. |