shoc-backend/BACKEND_ARCHITECTURE.md
Alexandre Brandizzi 7d245eb717
refactor: enforce backend boundaries and optimize dispatch (#30)
* refactor(api): enforce service and data-service boundaries

* refactor(api): complete feature service boundaries

* refactor(identity): enforce service and data boundaries

* refactor(vendors): enforce service and data boundaries

* refactor(workorders): enforce service and data boundaries

* refactor(backend): enforce architecture and optimize dispatch

* style(backend): format changed architecture files

* fix(architecture): address backend review follow-ups

* fix(backend): sanitize exception disclosure in changed API endpoints

Replace raw exception-message disclosure (ex.Message) returned to API
callers with a stable sanitized public message plus correlated structured
internal logging, across the endpoints changed in this PR.

- Add SanitizedErrors helper: logs the original exception at Error with a
  generated correlation id and returns a stable public message referencing
  it so support can trace without exposing internals.
- Inject ILogger<T> into the 14 changed controllers and route every
  ex.Message/dbex.Message disclosure through the helper, preserving status
  codes, response shapes, and business data (e.g. OpenWorkOrders).
- Leave FluentValidation (vex.Errors) and existing fixed-message catches
  untouched; out-of-scope controllers (Account/Contact/Employee/Asset/
  PMSchedule) are unchanged.
- Add focused tests proving internal exception text is not returned and
  that Error logging carrying the original exception is invoked.

* fix(architecture): abstract job run state access

* style: format board update service

* test: use collection assertion idiom
2026-07-24 17:35:34 -03:00

4.7 KiB

Backend Architecture and Quality Rules

This repository uses a feature-oriented service boundary over Entity Framework Core:

HTTP controller -> I{Feature}Service -> I{Feature}DataService -> ApplicationDbContext

The interfaces are architectural seams, not a requirement to wrap every class or every EF method. Add an interface when it separates HTTP, business, persistence, or external I/O responsibilities and enables behavior-focused testing.

Layer responsibilities

API controllers

  • Parse HTTP input, authorize the caller, invoke a business-service interface, and map the result to the established HTTP contract.
  • Do not inject ApplicationDbContext, concrete service implementations, or concrete data-service implementations.
  • Do not contain persistence queries or business workflows.
  • Do not expose exception messages to clients. Map known failures explicitly and let the centralized exception boundary handle unexpected failures.

Business services

  • Own validation, business decisions, orchestration, and transaction intent.
  • Depend on interfaces for persistence and external I/O.
  • Use IOptions<T> for typed configuration. Do not inject IConfiguration.
  • Keep feature responsibilities cohesive. Split a service when it has independently changing business reasons, not because it crossed an arbitrary line count.
  • Preserve existing API and board-backed behavior unless the accepted requirement explicitly changes it.

Data services

  • Own EF Core query shape, persistence, batching, and transaction implementation.
  • Accept cancellation tokens on new I/O methods and pass them to EF Core.
  • Use AsNoTracking() for read-only queries.
  • Project only required columns for list/read models; include full graphs only when the caller needs them.
  • Page unbounded collections at the database.
  • Avoid query-in-loop and save-in-loop patterns. Prefer set-based reads and batched writes while preserving the workflow's failure and transaction semantics.
  • Do not introduce a generic repository over EF Core. Feature data services should expose intent-revealing operations.

Performance review

For each changed hot path, document and test:

  • input size variables;
  • CPU and memory complexity;
  • database round trips;
  • whether filtering, ordering, and paging execute in SQL;
  • whether tracking is necessary;
  • whether repeated work can be hoisted out of loops;
  • the partial-failure and transaction behavior.

Prefer dictionary or hash-set reconstruction when database results must follow caller order. A query followed by dictionary reconstruction is normally O(n) CPU and memory; repeated First/Single scans over the result are O(n²).

Do not optimize by weakening correctness. In particular, batching all writes into one final save can change partial-success behavior, and adding a transaction can change locking and retry semantics. Those are business decisions, not mechanical cleanup.

Configuration

Configuration is bound once during composition in SeaHaven.Services/DependencyInjection/ServicesModule.cs. Business services receive typed options such as FrontendOptions, JwtOptions, ApprovalsOptions, and VendorPortalOptions.

Defaults must preserve current behavior. Startup validation should be introduced only when every deployed environment is known to provide the required value.

Enforcement

ArchitectureTests enforces mechanical dependency rules:

  • controllers cannot inject EF contexts or concrete service/data-service classes;
  • business services cannot inject EF contexts or raw IConfiguration.

The pull-request quality workflow runs those tests and verifies formatting for changed C# files. The normal CI workflow builds the solution and runs the full test suite.

Architecture tests are appropriate for dependency direction. Behavior belongs in tests through public interfaces. Do not add tests merely to prove a linter or analyzer itself works; test the product behavior the rule protects.

Review checklist

  • Controller depends only on service abstractions.
  • Business logic is in a cohesive feature service.
  • EF operations are behind a feature data-service abstraction.
  • No raw IConfiguration in a business service.
  • No unbounded load followed by in-memory filtering.
  • No query/save loop where a set-based operation preserves semantics.
  • Read-only EF queries use no tracking.
  • New async I/O accepts and propagates cancellation where the public contract permits.
  • Errors do not leak internal exception details.
  • Tests cover behavior, authorization, data contracts, ordering, duplicates, missing records, and relevant failure boundaries.
  • Changed behavior has been checked against the authoritative ticket board. Missing board access blocks readiness and merge claims.