Skip to main content

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

  1. Create New Service Package:

    • Create a new directory: backend/internal/services/remediation/.
    • Create a new file within it: remediation_service.go.
  2. Define the RemediationService:

    • In remediation_service.go, define the remediation.Service struct with a dependency on pg.TxManager.
    • Create a NewService constructor.
  3. Move Remediation Logic:

    • Carefully move all public and private functions related to remediation from results_service.go to the new remediation_service.go.
    • This includes CreateCorrectionBatch, ApplyCorrectionBatch, RevertCorrectionBatch, and their related helpers.
    • Update the function receivers from (s *ResultsService) to (s *Service).
  4. Rewire the HTTP Handler:

    • Modify backend/internal/http/handler/results_remediation.go to depend on and use the new remediation.Service instead of ResultsService.
  5. Update Dependencies:

    • In backend/cmd/server/main.go, instantiate the new remediation.Service and inject it into the NewResultsRemediationHandler (done).
  6. Update Event Listeners:

    • The RemediationService must trigger an event (e.g., "CorrectionBatchApplied") after a batch is successfully applied.
    • The ProgrammeProgressListener's OnCorrectionBatchApplied hook must be wired to consume this event.
    • This communication MUST be decoupled. The RemediationService should not have a direct, compile-time dependency on the ProgrammeProgressListener. An in-memory event dispatcher or a similar pattern should be used to adhere to the principles in ADR-0008.
  7. Add Focused Tests:

    • Created remediation_service_test.go with idempotent apply coverage and listener behavior.
  8. Verify and Cleanup:

    • go test ./... green; remediation logic removed from results_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

  1. Create New Service Packages:

    • Create backend/internal/services/buckets/.
    • Create backend/internal/services/sections/.
  2. Define New Services:

    • Created BucketService in backend/internal/services/buckets/ and SectionService in backend/internal/services/sections/.
  3. Move Logic:

    • Migrated bucket CRUD/preview/resolution to buckets.Service; section and section-item CRUD/ordering to sections.Service.
  4. 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).
  5. Rewire Handlers & Dependencies:

    • evaluation.go now calls BucketService/SectionService directly for buckets/sections/items.
    • cmd/server/main.go instantiates and injects bucket/section services alongside EvaluationService.
  6. 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)

  1. Create New Package:

    • Create a new directory: backend/internal/services/outcomes/.
  2. Define Core Interfaces:

    • Within this package, define Go interface types for the core concepts, such as ScoringEngine or ProgressTracker. 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 full EvaluationService, 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.
  3. Create Scaffolding:

    • Create empty Go files like scoring_engine.go within the new package to house future logic.
  4. Create Follow-up Tickets:

    • Document the plan to gradually migrate the relevant scoring and progress logic from ResultsService, RemediationService, and ProgrammeProgressListener into this new package over time. This avoids a "big bang" refactor while still establishing the clear architectural boundary for future work.

Current Module Boundaries (Conceptual Map)

SurfaceHandler DependencyService Interface / PackageNotes
Evaluations CRUD/versionsinternal/http/handler/evaluation.goservices.EvaluationServiceCore CRUD + version lifecycle.
Evaluations preview/validateinternal/http/handler/evaluation.goservices.EvaluationPreviewer (impl: EvaluationService)Interface boundary set; swap-friendly.
Buckets CRUD/previewinternal/http/handler/evaluation.go (BucketRoutes)buckets.ServiceFirst-class service; no EvaluationService coupling.
Sections/items CRUD/orderinternal/http/handler/evaluation.gosections.ServiceFirst-class service; reposition logic unit-tested.
Delivery sessions/resultsinternal/http/handler/evaluation.go (sessions)services.ResultsServiceNow lock-orders assignment/submission rows on session create.
Remediation (apply/revert)internal/http/handler/results_remediation.goremediation.ServiceIdempotent apply with listener hooks.
Outcomes (planned)n/a (to be wired)outcomes.ScoringEngine, outcomes.ProgressTrackerInterfaces added; implementations to follow.