Architecture Review

4:40 – 4:48 PM

We walk the finished Program.cs from top to bottom and name every decision made during the day. Nothing in it is accidental.

var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();                                    // M5: OTel, health, discovery

builder.Services.AddDbContext<RoasteryDbContext>(/* SQLite */); // M2
builder.Services.AddValidation();                                // M2: annotations are enforced
builder.Services.AddProblemDetails();                            // M2: one error shape
builder.Services.AddExceptionHandler<DomainExceptionHandler>();  // M2: domain errors → 409

builder.Services.AddIdentityCore<RoasteryUser>()/* … */;         // M3
builder.Services.AddAuthentication().AddJwtBearer(/* … */);      // M3
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("Staff", /* … */)
    .AddPolicy("OwnsOrder", /* … */);                            // M3: rules live here, not in handlers
builder.Services.AddRateLimiter(/* … */);                        // M3
builder.Services.AddCors(/* … */);                               // M3

builder.Services.AddOutputCache(/* … */);                        // M4: cache public responses
builder.Services.AddHybridCache();                               // M4: cache data in handlers
builder.Services.AddHostedService<OrderReadyNotifier>();         // M4: slow work off the request
builder.Services.AddHttpClient<PaymentClient>()
    .AddStandardResilienceHandler();                             // M4
builder.Services.AddApiVersioning(/* … */);                      // M5
builder.Services.AddOpenApi();                                   // M1

var app = builder.Build();

app.UseExceptionHandler();                                       // outermost: catches everything below
app.UseStatusCodePages();
app.UseHttpsRedirection();
if (!app.Environment.IsDevelopment()) app.UseHsts();
app.UseSecurityHeaders();                                        // M3
app.UseRateLimiter();                                            // before auth: cheap rejection first
app.UseCors("Frontend");
app.UseAuthentication();                                         // who
app.UseAuthorization();                                          // may they
app.UseOutputCache();                                            // after auth: never caches authenticated responses

var v1 = app.MapGroup("/api/v{version:apiVersion}")/* … */;      // M5
v1.MapProductEndpoints();                                        // public GETs cached; staff group locked
v1.MapOrderEndpoints();                                          // whole group authenticated
app.MapGroup("/api/auth").MapIdentityApi<RoasteryUser>().RequireRateLimiting("auth");
app.MapDefaultEndpoints();                                       // /health, /alive
app.MapOpenApi();
app.MapScalarApiReference();

app.Run();

public partial class Program { }                                 // M4: lets the tests see us

The decisions, named

Why endpoints are grouped by feature. A group is the unit you apply policy to: a tag, a filter, an authorization rule, a cache policy, a version. Feature folders keep Program.cs readable and make the policy surface visible in one place.

Why authorization is on groups rather than endpoints. A rule on the group means the next endpoint someone adds is protected by default. A rule per endpoint means it is open by default. Fail safe.

Why entities never leave the API. DTOs decouple the schema from the contract, prevent JSON cycles, and close OWASP API3 by making it impossible for a client to set Total or Status.

Why errors have one shape. Clients write one error handler. Binding failures, validation failures, domain rule violations, and unhandled exceptions all arrive as application/problem+json.

Why identity comes from the token, never the body. OWASP API1, Broken Object Level Authorization, is the most common API vulnerability there is, and the fix is to refuse to read who-you-are from what-you-sent.

Why caching lives at two layers. Output caching serves public responses without running a handler and refuses to cache authenticated ones. HybridCache speeds up lookups inside the handlers that do run. They share a tag so one eviction clears both.

Why the notifier is out of process. The barista pressing "Ready" should not wait for an email provider. A channel and a background service turn a slow side effect into a fast request.

Why the pipeline is in that order. The exception handler is outermost so it catches everything. Rate limiting is before authentication so abusive traffic is rejected cheaply. Output caching is after authorization so it can honor the rule about authenticated responses.

Why the tests go through HTTP. A test that calls a handler method directly skips binding, validation, filters, and authorization, which is most of what can go wrong.