Defensive Programming

Defensive programming is writing code that assumes inputs, clocks, disks, and other teams will lie. It is not “null-check everything” and not “never throw.” It is making implicit contracts explicit and failing in a way the caller can handle.

Trust boundaries

Draw the line: HTTP body, queue payload, file, environment, clock, “the other microservice.” Inside the line, prefer types that cannot represent illegal states. At the line, parse — do not cast.

// untrusted
var raw = request.Query["page"];
if (!int.TryParse(raw, out var page) || page < 1 || page > 1000)
    return Results.BadRequest();

Do not sprinkle int.Parse on query strings and hope.

Make illegal states unrepresentable

Replace boolean flags that contradict each other with a discriminated union or a small enum. An Order that is both Cancelled and Shipped is a bug factory. In C#, records + required members + private constructors beat public setters on every field.

sealed class FlaggedOrder
{
    public bool IsShipped { get; set; }
    public bool IsCancelled { get; set; } // both can be true
}

enum OrderStatus { Placed, Shipped, Cancelled }

sealed record StatusOrder(OrderStatus Status);

FlaggedOrder can be shipped and cancelled at once. StatusOrder cannot.

Fail closed vs fail open

Authorization, payments, and privacy: fail closed (deny if unsure). Observability and optional recommendations: fail open with a metric. Defaulting “allow” on a missing role claim is an incident.

Assertions, not business control flow

Use assertions / Debug.Assert / “this is a programmer error” exceptions for broken invariants (null repository in a composition root). Use domain results or HTTP 4xx for user-fixable mistakes. Catching Exception around a whole request and returning 200 hides outages.

Timeouts, cancellation, idempotency

Pass CancellationToken. Honor it in I/O. Treat client disconnect as cancel, not as “keep writing the report.” For handlers that may run twice (at-least-once queues, user double-submit), store an idempotency key. See reliability patterns.

What breaks it

The catch turns a downed gateway into HTTP 200. Delete it: the fault stays a 500. Return a 4xx only for input the caller can fix.

static IResult HideTheFault()
{
    try
    {
        throw new InvalidOperationException("gateway down");
    }
    catch (Exception)
    {
        return Results.Ok();
    }
}

Pitfalls

  • Swallowing exceptions to “keep the app up.”
  • Logging tokens, PAN, or session cookies “for debug.”
  • Defensive copies nowhere, then mutating a cached DTO.
  • Validating only on the client.

Related: OWASP Top Ten, SQL for Business Logic.