Graceful degradation sounds simple: if one dependency is unavailable, keep the independent parts of the application running.
That is the right goal. The trap is treating degraded mode as one boolean that turns a feature off. In a routed web application, each endpoint sits inside a graph of framework services, middleware, policies, and downstream systems. Removing one dependency does not remove the others.
A recent committed change made that distinction unusually clear.
One extra path segment changed the result
The host had two broad capabilities:
- public pages that could render without a database;
- a private area backed by a database and an identity store.
The desired degraded state was reasonable. If the database configuration was absent, public pages should remain available and the private capability should report that it was unavailable.
An initial fallback middleware checked the private path prefix and returned a controlled response. A nearby child URL produced the expected result, so the approach appeared sound.
The exact protected route behaved differently. It matched a real endpoint decorated with an authorization requirement. The framework reached endpoint authorization before the fallback could compensate. Because no authentication scheme had been registered in that configuration, the request ended as a bare 500.
The lookalike URL had tested a different pipeline.
Degraded mode is a dependency graph
The public pages had shed their database dependency. The protected endpoint had not shed its dependency on ASP.NET Core's authentication and authorization contracts.
That leads to a useful rule:
If an endpoint remains mapped, keep the framework contracts required to process it valid in every supported configuration.
For a protected route, “valid” does not mean pretending that sign-in can succeed. It means the security pipeline remains coherent and fails closed.
A simplified shape looks like this:
if (identityStoreIsAvailable)
{
services.AddTheRealIdentityStore();
}
else
{
services.AddAuthentication("Unavailable")
.AddCookie("Unavailable");
}
services.AddAuthorization();
Authentication and authorization middleware can then remain unconditional. In the unavailable branch, nobody can become authenticated because there is no identity store behind the fallback scheme. The protected endpoint challenges safely instead of throwing or accidentally opening.
The important idea is not this exact registration. It is the explicit unavailable implementation. Missing infrastructure is represented as a supported state rather than as an incomplete application.
Give each route an honest outcome
Once the security pipeline was coherent, the application could express several outcomes deliberately:
- independent public pages continued to return success;
- database-dependent access paths returned an honest unavailable response;
- an anonymous request in the healthy configuration was challenged normally;
- no protected route failed open;
- no supported configuration produced an unhandled exception.
This is more precise than asking whether “the site” is up. Parts of the host can have different availability without weakening the security boundary between them.
Liveness and readiness answer different questions
There is an operational trade-off. If public pages are intentionally database-free, a liveness probe can remain green while the private capability is unavailable.
That is not dishonest if the probes are named and monitored correctly:
- Liveness: Is the process running and able to serve independent work?
- Readiness: Are the dependencies required for the full capability available?
The unavailable state must also be loud in logs. Otherwise graceful degradation can turn into silent degradation: users see only the healthy surface while operators miss a broken private one.
This design adds configuration branches, a fallback scheme, route-specific responses, and monitoring semantics. The return is a smaller failure domain and a security posture that remains fail-closed.
Turn the discovery into a route matrix
The most valuable follow-up is an integration-test matrix built around exact endpoints, not representative-looking strings.
For each supported dependency state, exercise:
- an independent public route;
- the exact protected route;
- a near-miss or unmatched route;
- the sign-in entry point;
- liveness and readiness.
Assert the intended status or redirect for each cell. Also assert two negative properties across the whole matrix: no 500 responses and no authorization bypass.
The committed change was manually checked across configured and unconfigured states. That was enough to expose the framework-ordering mistake. Encoding the same matrix as integration tests would turn the discovery into a durable contract.
Practical takeaway
When designing graceful degradation, list capabilities first, then trace every mapped route through routing, authentication, authorization, storage, and external services. Decide which dependencies each route may shed and which framework contracts must remain intact.
Finally, test the exact route through the real pipeline. A lookalike URL can make a broken fallback look correct.
Top comments (0)