A demo solution in .NET 9 implementing Clean Architecture, CQRS with MediatR, and modern engineering practices.
It simulates two bounded contexts:
- Vehicles: manages vehicle data (lookup by reg number, batch lookup)
- Insurance: manages insurance policies and enriches them with vehicle details
The project demonstrates testability, separation of concerns, feature toggling, centralized error handling, coalescing to avoid duplicate calls, and extensibility.
- Architecture
- Running the Solution
- Docker Quick Start
- Endpoints
- Seed Data
- Testing
- Error Handling
- Performance Considerations
- Modern Engineering Practices
- Security (Roadmap)
- CI/CD & DevOps
- Extensibility
- Troubleshooting
- TODO Roadmap
- Architecture Decisions
- How AI Helped
- Personal Reflection
We follow Clean Architecture with four layers:
graph LR
subgraph "Clients"
CLI[HTTP Clients]
CURL[cURL/Postman]
WEB[Web Apps]
end
subgraph VEHICLE_SERVICE["π Vehicle Service"]
direction TB
subgraph V_API["API Layer"]
VAPI[Controllers]
VSWAG[Swagger/Health]
VMID[Middleware]
end
subgraph V_APP["Application Layer"]
VMED[MediatR Handlers]
VPORTS[Ports/Interfaces]
VPIPE[Pipeline Behaviors]
end
subgraph V_DOMAIN["Domain Layer"]
VENT[Vehicle Entity]
VREG[RegistrationNumber VO]
VDOM[Domain Rules]
end
subgraph V_INFRA["Infrastructure Layer"]
VEF[EF Core]
VMEM[In-Memory Store]
end
end
subgraph INSURANCE_SERVICE["π‘οΈ Insurance Service"]
direction TB
subgraph I_API["API Layer"]
IAPI[Controllers]
ISWAG[Swagger/Health]
IMID[Middleware]
end
subgraph I_APP["Application Layer"]
IMED[MediatR Handlers]
IPORTS[Insurance Ports]
VPORT[Vehicle Lookup Port]
IPIPE[Pipeline Behaviors]
end
subgraph I_DOMAIN["Domain Layer"]
IENT[Policy Entity]
IPER[PersonalNumber VO]
IDOM[Domain Rules]
end
subgraph I_INFRA["Infrastructure Layer"]
IEF[EF Core]
IMEM[In-Memory Store]
subgraph DECORATORS["Decorator Chain"]
IFEAT[Feature Gate]
ICOAL[Coalescing]
IHTTP[HTTP Adapter]
end
end
end
subgraph "Database"
POSTGRES[(PostgreSQL)]
end
%% Client connections
CLI --> VAPI
CLI --> IAPI
CURL --> VAPI
CURL --> IAPI
WEB --> IAPI
%% Vehicle Service internal flow
VAPI --> VMED
VMED --> VPORTS
VPORTS --> VEF
VPORTS --> VMEM
VPIPE --> VMED
%% Insurance Service internal flow
IAPI --> IMED
IMED --> IPORTS
IMED --> VPORT
IPORTS --> IEF
IPORTS --> IMEM
IPIPE --> IMED
%% Service-to-Service Communication
VPORT --> IFEAT
IFEAT --> ICOAL
ICOAL --> IHTTP
IHTTP -.->|HTTP Call| VAPI
%% Database connections
VEF --> POSTGRES
IEF --> POSTGRES
%% Styling for GitHub compatibility (dark text on light backgrounds)
classDef apiLayer fill:#bbdefb,stroke:#1565c0,stroke-width:2px,color:#000000
classDef applicationLayer fill:#d1c4e9,stroke:#512da8,stroke-width:2px,color:#000000
classDef domainLayer fill:#c8e6c8,stroke:#388e3c,stroke-width:2px,color:#000000
classDef infraLayer fill:#ffe0b2,stroke:#f57c00,stroke-width:2px,color:#000000
classDef external fill:#ffcdd2,stroke:#d32f2f,stroke-width:2px,color:#000000
classDef client fill:#dcedc8,stroke:#689f38,stroke-width:2px,color:#000000
classDef service fill:#e0e0e0,stroke:#424242,stroke-width:3px,color:#000000
class VAPI,VSWAG,VMID,IAPI,ISWAG,IMID apiLayer
class VMED,VPORTS,VPIPE,IMED,IPORTS,VPORT,IPIPE applicationLayer
class VENT,VREG,VDOM,IENT,IPER,IDOM domainLayer
class VEF,VMEM,IEF,IMEM,IHTTP,IFEAT,ICOAL infraLayer
class POSTGRES external
class CLI,CURL,WEB client
class VEHICLE_SERVICE,INSURANCE_SERVICE service
- Separation of concerns β testable and maintainable
- Frameworks (ASP.NET, EF, etc.) are details, not core
- Domain is pure C#
- Easier to extend (new APIs, workers, persistence) without touching business rules
- CQRS request/response pattern (queries & commands)
- Centralized pipeline behaviors for validation & logging
- Keeps controllers thin β orchestration belongs to use cases
Enables safe rollout of new features:
- Vehicles API: toggle endpoints (
/batch) with[FeatureGate] - Insurance API: toggle enrichment via decorator (short-circuits if disabled)
Flags are configured in appsettings.json:
{
"FeatureManagement": {
"EnableVehiclesBatchEndpoint": true,
"EnableInsuranceEnrichment": true
}
}Without it, multiple concurrent requests for the same car regs would trigger duplicate HTTP calls.
Using a SingleFlightCoordinator, concurrent requests for the same key are merged into one upstream call. This avoids the N+1 problem and reduces load on the Vehicles API.
Decorator chain for Insurance outbound calls:
FeatureGatedVehicleLookup (outermost, skips if flag OFF)
β CoalescingVehicleLookupAdapter (merges duplicates in-flight)
β VehicleLookupHttpAdapter (real HTTP to Vehicles API)
- .NET 8 SDK (for local development)
- Docker Desktop (for containerized deployment)
dotnet restore
dotnet build
# Run Vehicles API (http://localhost:5011/swagger)
dotnet run --project src/Vehicles.Api
# Run Insurance API (http://localhost:5021/swagger)
dotnet run --project src/Insurance.ApiEverything below runs via Docker & docker-compose β no local .NET SDK required.
- Docker Desktop (Windows/macOS) or Docker Engine + Compose v2 (Linux)
- Ability to pull images from mcr.microsoft.com
- Host ports 8081 and 8082 open
# Build and start (first run may take a few minutes)
docker compose up -d --build
# See containers
docker compose ps
# Tail logs
docker compose logs -f postgres
docker compose logs -f vehicles.api
docker compose logs -f insurance.apiNote: EF Core migrations run automatically on startup when MIGRATE_ON_STARTUP=true. Postgres databases are initialized via SQL in infra/db/init/.
- Vehicles Swagger: http://localhost:8081/swagger
- Insurance Swagger: http://localhost:8082/swagger
- Health Checks:
- Vehicles β http://localhost:8081/health
- Insurance β http://localhost:8082/health
# Vehicles: single lookup
curl -s http://localhost:8081/v1/vehicles/ABC123 | jq
# Vehicles: batch lookup (feature-flagged)
curl -s -X POST http://localhost:8081/v1/vehicles/batch \
-H "Content-Type: application/json" \
-d '{"regs":["ABC123","XYZ999","MISSING"]}' | jq
# Insurance: summary (enriched with vehicles)
curl -s http://localhost:8082/v1/insurances/19650101-1234 | jq
# Health checks
curl -i http://localhost:8081/health
curl -i http://localhost:8082/healthVehicles API
GET /v1/vehicles/{reg}β single vehicle by reg numberPOST /v1/vehicles/batchβ batch lookup (feature-toggled)
Insurance API
GET /v1/insurances/{personalNumber}β insurance summary, optionally enriched with vehicles
| RegNumber | Make | Model | Year | Vin |
|---|---|---|---|---|
| ABC123 | Tesla | Model 3 | 2020 | VIN-A |
| XYZ999 | Volvo | XC90 | 2019 | VIN-X |
| KLM456 | Toyota | Corolla | 2018 | VIN-K |
| Person Number | PolicyType | MonthlyCost | VehicleRegNumber |
|---|---|---|---|
| 19650101-1234 | Pet | 10 USD | β |
| 19650101-1234 | PersonalHealth | 20 USD | β |
| 19650101-1234 | Car | 30 USD | ABC123 |
| 19650101-1234 | Car | 30 USD | ABC123 |
| 19650101-1234 | Car | 30 USD | XYZ999 |
| 19700101-1111 | Pet | 10 USD | β |
| 19700101-1111 | PersonalHealth | 20 USD | β |
dotnet testDomain unit tests: Entities + Value Objects (pure C#, fast feedback).
Application tests: MediatR handlers, validators, pipeline behaviors (validation/logging).
Infrastructure tests:
- EF Core with SQLite (fast translation checks)
- Migrations smoke with Testcontainers Postgres to validate schema + constraints
- HTTP adapters & decorators (coalescing, feature flags) with fakes
API integration tests: Boot real hosts via WebApplicationFactory. Replace EF/HTTP with in-memory fakes for deterministic behavior.
- Broad base of unit tests
- Fewer integration tests
- Only a handful of API tests (end-to-end)
Coverage is collected in GitHub Actions and summarized in the build output.
Centralized via ProblemDetails (RFC 7807):
| Exception | HTTP Code | Title |
|---|---|---|
| ValidationException | 400 | Validation error |
| DomainException | 422 | Domain error |
| HttpRequestException | 502 | Upstream error |
| all others | 500 | Server error |
The assignment requires only two endpoints, but I implemented an optional POST /v1/vehicles/batch for two reasons:
Performance realism: In real systems, the Insurance service often needs multiple vehicle records at once. A batch call allows fewer TCP connections, fewer round trips, and better upstream optimization.
Clear separation of concerns: The public contract can remain "one vehicle by reg," while internal consumers (like Insurance) can use batch for efficiency. The batch endpoint is feature-flagged so you can disable it and still fully meet the brief.
Even with only GET /v1/vehicles/{reg}, we can avoid the classic "N requests β N upstream calls" bottleneck through:
Client-side dedup + limited parallelism: Before calling Vehicles, deduplicate regs (case-insensitive) and use Task.WhenAll with bounded parallelism to avoid stampeding the Vehicles API.
In-flight de-duplication (SingleFlight): Already implemented as a decorator. If 10 requests for ABC123 arrive concurrently, only one upstream call is made; the rest await the same task.
Caching via decorator: Wrap the Vehicles port with a cache decorator. For demo data like vehicle metadata (rarely changes), a short TTL cache provides significant performance gains.
Resilience & backpressure: Polly retries with jitter, timeouts, and circuit breakers to protect the Vehicles API. Bulkhead patterns cap concurrent outbound calls.
Current:
- Coalescing to dedupe concurrent calls
- Feature flags for safe rollout
- MediatR pipeline for validation/logging
- EF Core with migrations & seed data
Roadmap:
- Resilience with Polly: retry (with jitter), circuit breaker, timeouts
- Caching: decorator with IMemoryCache or Redis; edge cache via gateway/CDN
- Observability: OpenTelemetry traces/metrics/logs β Grafana Tempo/Loki/Prometheus; correlation IDs
Current: Centralized error handling β 400 validation, 422 domain, 502 upstream, 500 server.
Next steps:
- AuthN/Z: JWT bearer (Auth0/AWS Cognito/Azure AD) + policies/scopes
- Rate limiting: AddRateLimiter (fixed window/token bucket)
- Secret management: GitHub secrets for CI; AWS SSM/Secrets Manager in prod
- Headers/TLS: HTTPS redirect, HSTS, and secure headers (via middleware or gateway)
CI (present): GitHub Actions (build-and-test.yml): restore, build (warnings as errors), tests, coverage, artifacts.
CD (TODO) β AWS example:
- Infra with AWS CDK: VPC, ECS Fargate (or App Runner), ALB, RDS Postgres, ECR repos, SSM/Secrets
- Pipeline: Build and push images to ECR, aws-actions/configure-aws-credentials, cdk deploy
- Missing vehicles β policy still returned, vehicle = null
- No insurances β empty list, cost = 0
- Multiple insurances β aggregated monthly cost, deduplicated vehicle lookups
- Current APIs use
/v1/... - Future versions can live alongside
/v2/...without breaking clients
- CD to AWS via CDK (ECS/App Runner + RDS)
- Caching (infra decorator or edge cache)
- Resilience with Polly (retry, circuit breaker)
- Rate limiting (AddRateLimiter)
- Security (auth, headers, secrets mgmt)
- Observability (OpenTelemetry β Grafana stack)
- Makefile (
make up) for one-command run
- Clone the repo & install .NET 8
- Run the APIs locally (
dotnet run) - Explore Swagger:
- Vehicles β http://localhost:5011/swagger
- Insurance β http://localhost:5021/swagger
- Use seed data above to try requests
- Run tests (
dotnet test)
- Clone the repo
- Start the stack:
docker compose up -d --build - Explore Swagger:
- Vehicles β http://localhost:8081/swagger
- Insurance β http://localhost:8082/swagger
- Try the sample curl requests
- Run tests using the SDK container (no local SDK needed):
# bash/zsh (macOS/Linux): docker run --rm -v "$PWD:/src" -w /src mcr.microsoft.com/dotnet/sdk:9.0 dotnet test # PowerShell (Windows): docker run --rm -v "${PWD}:/src" -w /src mcr.microsoft.com/dotnet/sdk:9.0 dotnet test
- Stop:
docker compose down
- MediatR for orchestration (CQRS)
- Pipeline Behaviors for Validation & Logging
- ProblemDetails for consistent error responses
- Ports/Adapters to keep APIs decoupled
- Feature Flags for safe rollout
- EF Core with Postgres for persistence
- Coalescing (SingleFlight) to merge concurrent outbound calls
AI was used during development to:
- Bootstrap boilerplate: e.g., pipeline behaviors, DTOs, Swagger setup
- Testing support: generating unit + integration test scaffolds quickly
- Code generation: repetitive DI extension methods, ProblemDetails handler
- Performance ideas: guiding the implementation of coalescing to avoid duplicate HTTP calls
- Docker setup: generating compose configurations and Dockerfiles
It accelerated delivery but all code was reviewed, refined, and adapted manually to ensure correctness and clarity.
In my current role, we also use Clean Architecture and microservices, so this assignment felt familiar. The most interesting part was tackling performance and avoiding N+1 queries, especially through coalescing and batching. The EF integration stayed clean by mapping at the infrastructure edge and keeping the domain pure.
If I had more time, I'd implement the remaining TODOs (CD with CDK, resilience with Polly, caching decorators, and observability) to make the solution closer to production-ready.
CI runs on every push/PR:
- Build with warnings-as-errors
- Run all tests
- Collect coverage
- Publish summary in GitHub Actions UI
Built with β€οΈ