Modern .NET backend · STS

Owning a backend from PDF requirements to deployable service.

A public-sector assessment system where I owned the backend shape, local development setup, authentication flow, scoring rules, CI/CD, and deployment path.

What it was

The project was a web-based assessment service for the State Probation Service. Users answered tests and received feedback about risk level and related results. I first received the work as database schema and Swagger planning from a PDF, then later became the backend developer responsible for building the service.

The public description is simple. The useful engineering work was turning a thin requirement source into a backend with clear contracts, versioned domain behavior, authentication, deployment, and enough test coverage to keep the most important rules honest.

My role

I wrote the backend from scratch rather than using the older company template. I also created the TFS repositories for the backend and frontend applications, prepared the Azure environment, set up CI/CD, and handled the authentication path across local development, company infrastructure, and client configuration.

  • Designed the backend from sparse PDF requirements rather than an existing codebase.
  • Built a JSON scoring-rules engine instead of allowing raw SQL to be uploaded from the admin panel.
  • Owned authentication across local development, company Keycloak, client Keycloak, and frontend integration points.
  • Set up repositories, Azure environment, CI/CD, Docker image publishing, and deployment flow.
  • Kept domain failures explicit through stable error codes and ProblemDetails responses covered by integration tests.

Architecture

The backend was a .NET 10 ASP.NET Minimal API application with a feature-sliced core. Features such as content, tests, sessions, scoring, and assets owned their domain types, contracts, commands, queries, projections, specifications, and policies close to the behavior they supported.

Commands and queries went through Mediator handlers. Reads used projections/specifications where appropriate; write paths used repositories and unit-of-work rules. Foundation tests included architecture checks, for example preventing query handlers from depending on write repositories and requiring a unit of work when a handler used multiple write repositories.

Aspire composed local PostgreSQL and pgAdmin for development. The application was containerized and wired into a CI/CD workflow that restored locked packages, built the API, ran foundation and integration tests, built a Docker image, pushed it to a registry, and deployed it through Docker Compose.

Scoring rules

One important decision was pushing back on raw SQL uploaded from the admin panel. Instead, I built a JSON-based scoring rules system. Rules could describe ordered cases, fallback categories, boolean composition, comparisons, ranges, constants, trait totals, and max-expression behavior.

Tests were versioned. Publishing and then creating a new draft cloned the domain structure, including trait IDs, and the scoring JSON had to be remapped so expressions pointed to the cloned version rather than the previous one.

Error flow

Domain failures used explicit error kinds and stable error codes. Middleware converted those into ProblemDetails responses with HTTP status, detail, request path, trace ID, code, and structured extensions when needed. Repository helpers kept not-found behavior consistent across handlers.

Integration tests asserted exact ProblemDetails codes for validation, missing resources, conflicts, asset upload failures, and scoring-rule integrity errors. This made failure behavior part of the contract instead of an accidental side effect.

State when I left

The core backend and deployment path were in place. The major unfinished area was logging and statistics for later analysis: completed-test counts, average results, and similar reporting. I had started that work, but it was large enough to deserve proper design rather than a small patch.

What it proves

This is the cleanest proof that I can own a modern backend beyond controller code: translate incomplete requirements into a domain model, make uncomfortable product decisions visible, build the service, wire authentication, protect important rules with tests, and carry the system through CI/CD and deployment.