Clean Architecture
What it is
ManpowerIQ's backend follows Clean Architecture: concentric layers with a strict dependency rule — outer layers depend on inner ones, never the reverse. The Domain sits at the centre; the API and Infrastructure are the outermost wiring.
Important — there is no CQRS or MediatR. A .NET reader sees "Clean Architecture" and reasonably assumes a
MediatRrequest/handler pipeline. ManpowerIQ does not use it. There are noIRequest/IRequestHandler/IMediatortypes in the codebase. The architecture is service-based: controllers depend on and call business service classes directly through constructor injection. (Verified 2026-06-10: grep for MediatR/IRequestHandler/IMediator → no matches.)
Why it's built this way
The dependency rule exists so the business core is independent of delivery and persistence. The Domain (entities, the rules' shapes) and the Application (services, use cases) know nothing about EF Core, Postgres, ASP.NET, or JWTs. That independence is what lets the allocation engine's rules and the demand state machine be unit-tested with no database, and lets infrastructure choices change without touching the rules.
MediatR was not adopted: the team kept the call path explicit — controller → service → DbContext — rather than routing everything through a mediator. The trade-off is fewer indirection layers and a direct, greppable call graph, at the cost of the cross-cutting-behaviour pipeline MediatR would offer (which is instead handled by middleware and EF interceptors).
How it works
flowchart TB
subgraph API[API · outermost]
C[Controller<br/>permission-guarded]
end
subgraph APP[Application]
I[Interfaces<br/>ICurrentTenantProvider, IAuditLogger]
S[Service classes<br/>business rules]
end
subgraph DOM[Domain · centre]
E[Entities + enums]
end
subgraph INFRA[Infrastructure · outermost]
IMPL[EF DbContext, CurrentTenantProvider,<br/>AuditLogger, JwtTokenMinter]
end
C --> S
S --> E
S --> I
IMPL -. implements .-> I
IMPL --> E
- Domain depends on nothing. Entities derive from
AuditableEntity/TenantEntity(sheet 01 §entities). - Application depends only on Domain. It defines the interfaces for things it needs from the outside — e.g.
ICurrentTenantProvider(Application/Tenancy/),IAuditLogger(Application/Auditing/) — and contains the service classes that hold the business rules. - Infrastructure depends on Application + Domain, and implements those interfaces:
CurrentTenantProvider(sheet 01 §build-status,Program.cs:179),AuditLogger(AuditLogger.cs:30-65), the EFManpowerIQDbContext,JwtTokenMinter. This inversion (Application owns the interface, Infrastructure owns the implementation) is the Dependency Inversion Principle in practice. - API depends on Application + Infrastructure. Controllers receive services by DI and call them directly; the request pipeline and DI graph are composed in
Program.cs.
Cross-cutting concerns that a MediatR pipeline would carry are handled elsewhere instead:
- Tenant stamping / isolation → an EF
SaveChangesinterceptor + middleware (see Multi-tenancy). - Authorization → a permission policy provider + middleware (see Authentication & RBAC).
- Auditing → an explicit, manually-invoked
IAuditLoggercall (see Audit & soft-delete).
Gotchas / constraints
- Do not add an Application → Infrastructure reference. Application defines interfaces; Infrastructure implements them. Keeping the arrow pointing inward is the whole point.
- Don't reach for MediatR to add a behaviour. There is no pipeline to plug into. Cross-cutting logic goes in middleware or EF interceptors, matching the existing pattern.
- Audit is not an automatic cross-cutting concern. Because there's no interceptor for it, a new write that should be audited must call
IAuditLoggerexplicitly (sheet 01 MUST-NOT #3).
Build status
Available — the layered, service-based architecture is the live structure. CQRS/MediatR is Planned/absent (not used).
Related
- Solution layout
- Multi-tenancy, Authentication & RBAC, Audit & soft-delete — where the cross-cutting concerns live.
- Fact sheet: 01 (foundation).