📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What does fail fast mean in .NET, and how do you apply it without making the system fragile?

Detect invalid state at the earliest point: guard clauses, ArgumentNullException.ThrowIfNull, options validation on start, ValidateOnBuild; fail fast versus resilience at boundaries.

Mid-Level Theory Intermediate configuration design-principles fail-fast validation

🎯 Key points

  • Detect invalid input or state as early as possible and stop loudly
  • Guard clauses: ArgumentNullException.ThrowIfNull, ArgumentOutOfRangeException.ThrowIfNegative
  • Validate configuration at startup with ValidateDataAnnotations and ValidateOnStart
  • ValidateOnBuild and ValidateScopes catch DI mistakes at startup
  • Nullable reference types and constructors that enforce invariants move failures to compile or creation time
  • Fail fast inside a component; be resilient at boundaries with retries and graceful degradation

⏱ 30-second answer

Fail fast means detecting a problem at the earliest point and stopping clearly, instead of carrying bad state forward until it causes a confusing failure later. In .NET that means guard clauses (ArgumentNullException.ThrowIfNull, ArgumentOutOfRangeException.ThrowIfNegativeOrZero), constructors that refuse invalid objects, nullable reference types, options validation with ValidateOnStart(), and DI ValidateOnBuild. It does not mean crashing on every transient error: inside a component I fail fast on programming and configuration errors, and at boundaries (network, other services) I use retries, timeouts and graceful degradation.

🗣 Full answer (senior)

How I would answer this in an interview

"Fail fast means detecting a problem at the earliest possible point and stopping loudly and clearly, instead of carrying bad state forward until it causes a confusing failure somewhere else, often hours later and far from the cause. In .NET I push failures as early as they can go. At compile time: nullable reference types, required members, strong types and value objects. At startup: options validated with ValidateDataAnnotations().ValidateOnStart(), and DI checked with ValidateOnBuild/ValidateScopes, so a broken deployment fails its readiness probe instead of taking traffic. At object creation: constructors and factories that refuse invalid objects. At method entry: guard clauses like ArgumentNullException.ThrowIfNull and ArgumentOutOfRangeException.ThrowIfNegativeOrZero. The part that keeps it from making systems fragile is classifying failures. Programming errors and misconfiguration should fail fast. Transient faults at boundaries (a slow partner API, a dropped connection) should be handled with timeouts, retries, circuit breakers and graceful degradation. And Environment.FailFast is reserved for state that's unsafe to continue with."


1. Where to fail fast

Stage .NET mechanism Catches
Compile time Nullable reference types, required members, init, strong types Nulls, missing initialisation, primitive mix-ups
Startup ValidateOnStart() for options; ValidateOnBuild / ValidateScopes for DI Missing/invalid configuration, missing registrations, captive dependencies
Object creation Constructor/factory guards, value objects (Abn, Money, Email) Invalid objects existing at all
Method entry ThrowIfNull, ThrowIfNullOrWhiteSpace, ThrowIfNegativeOrZero, ThrowIfGreaterThan Invalid arguments
API boundary Model validation → 400 ProblemDetails Bad client input
Impossible state throw new UnreachableException() (.NET 7+), Debug.Assert Logic bugs
Catastrophic corruption Environment.FailFast(message) State unsafe to continue with

2. Fail fast vs resilience

Failure Category Response
Missing connection string / invalid URL setting Configuration Fail at startup, block the rollout
Missing DI registration Programming ValidateOnBuild fails Build()
Null/invalid argument from a caller Programming Guard clause → ArgumentException
Invalid client input Expected 400 with details (fail fast for that request, not the process)
Partner API timeout Transient Retry with backoff + jitter, timeout
Dependency down for minutes Environmental Circuit breaker, fallback, degrade
Corrupted critical in-memory state Catastrophic Environment.FailFast (skips finally, writes crash dump)

The rule of thumb: fail fast inside a component for things that indicate a bug or misconfiguration; be resilient at boundaries for things the environment does.

3. Fail fast and deployment safety

Without With
App starts, health checks green, first real request fails App refuses to start with a clear OptionsValidationException
Bad config discovered by a customer Readiness probe fails; Kubernetes/slots stop the rollout
Rollback after customer impact Rollout never completes; no impact

Production example

Our [checkout service] was deployed with a mistyped payment configuration key. The app started, health checks were green, and nothing happened until the first customer tried to pay [40 minutes] later and got an error from a null gateway URI. The options were bound but never validated, so the failure surfaced only when the gateway client was first created. I added data annotations ([Required, Url], [Range]) to the options class and registered it with ValidateDataAnnotations().ValidateOnStart(), turned on ValidateOnBuild and ValidateScopes, and made the readiness probe depend on successful startup. We also validated configuration files against a schema in CI. The next misconfiguration failed the rollout within seconds and never reached customers.

🔍 What the interviewer is checking

Checks defensive programming and operational maturity.

Green flags

  • Names modern throw helpers and ValidateOnStart.
  • Distinguishes bugs from transient faults.
  • Links fail fast to deployment safety and readiness probes.
  • Warns about swallowed exceptions.

Red flags

  • Wraps everything in try/catch and logs.
  • Thinks fail fast means crashing on any error.

📚 Learn in depth

1. Origin

Jim Shore's Fail Fast (IEEE Software, 2004) argued that code which fails immediately and visibly is more robust, not less, because defects are found and fixed instead of hiding. The same idea appears in Erlang's "let it crash" philosophy, paired with supervision: fail fast locally, recover at a higher level.

2. Why late failures are expensive

Controller (null customerId accepted)
  → Service (passes it along)
    → Repository (builds a query)
      → Mapper → NullReferenceException in an unrelated place, minutes or hours later

The stack trace points at the symptom, not the cause; logs lack the original context. A guard at the controller or service boundary would have produced ArgumentNullException: customerId immediately.

3. Throw helpers

Helper Since
ArgumentNullException.ThrowIfNull .NET 6
ArgumentException.ThrowIfNullOrEmpty .NET 7
ObjectDisposedException.ThrowIf .NET 7
ArgumentException.ThrowIfNullOrWhiteSpace .NET 8
ArgumentOutOfRangeException.ThrowIfNegative, ThrowIfNegativeOrZero, ThrowIfZero, ThrowIfGreaterThan, ThrowIfLessThan, ThrowIfEqual... .NET 8

They use [CallerArgumentExpression] to fill in the parameter name automatically and keep the throwing code out of the hot path (better inlining).

4. Options validation in depth

builder.Services.AddOptions<PaymentOptions>()
    .BindConfiguration("Payments")
    .ValidateDataAnnotations()
    .Validate(o => o.TimeoutSeconds < o.TotalBudgetSeconds, "Timeout must be below the total budget")
    .ValidateOnStart();
  • ValidateOnStart runs validators during host startup (before requests).
  • Custom IValidateOptions<T> handles cross-field rules.
  • .NET 8 adds a source-generated options validator ([OptionsValidator]) that's trimming/AOT-friendly.

5. Value objects: fail fast at creation

public readonly record struct Email
{
    public string Value { get; }
    public Email(string value)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(value);
        if (!value.Contains('@')) throw new ArgumentException("Invalid email", nameof(value));
        Value = value.Trim();
    }
}

Once an Email exists it's valid everywhere, so downstream code needs no re-checks.

6. Don't swallow exceptions

try { ProcessPayment(); }
catch (Exception) { }          // the opposite of fail fast: bad state continues silently

Catch only exceptions you can handle meaningfully, add context, and let the rest reach the global handler (IExceptionHandler, BackgroundService exception behaviour).

7. Environment.FailFast

  • Terminates the process immediately, logs the message to the event log/stderr and can produce a crash dump.
  • Skips finally blocks, finalisers and graceful shutdown.
  • Use when continuing could corrupt data (e.g. detected memory/state corruption in a critical component), never for ordinary errors.

8. Try it yourself

  1. Run the code example without Payments:BaseUrl and observe the startup failure.
  2. Register a service that depends on an unregistered type with ValidateOnBuild on, and see Build() fail.
  3. Replace manual if (x == null) throw ... checks with ThrowIfNull and compare messages.
  4. Introduce a value object for one primitive (email, ABN) and delete downstream re-validation.
  5. Search for empty catch blocks and decide what each should really do.

💻 Code

using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOptions<PaymentOptions>()
    .BindConfiguration("Payments")
    .ValidateDataAnnotations()
    .ValidateOnStart(); // misconfiguration stops startup, not the first payment

builder.Host.UseDefaultServiceProvider(o =>
{
    o.ValidateScopes = true;
    o.ValidateOnBuild = true; // missing registrations fail at Build()
});

builder.Services.AddSingleton<RefundCalculator>();
var app = builder.Build();

app.MapGet("/refund/{amount:decimal}", (decimal amount, RefundCalculator calc, IOptions<PaymentOptions> opt) =>
    Results.Ok(new { refund = calc.Refund(amount, 0.1m), gateway = opt.Value.BaseUrl }));

app.Run();

public sealed class PaymentOptions
{
    [Required, Url] public string BaseUrl { get; set; } = "";
    [Range(1, 60)] public int TimeoutSeconds { get; set; } = 10;
}

public sealed class RefundCalculator
{
    public decimal Refund(decimal amount, decimal feeRate)
    {
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
        ArgumentOutOfRangeException.ThrowIfNegative(feeRate);
        ArgumentOutOfRangeException.ThrowIfGreaterThan(feeRate, 1m);
        return Math.Round(amount * (1 - feeRate), 2);
    }
}

Run without a Payments:BaseUrl setting and the host throws OptionsValidationException at startup.

⚠ Common mistakes

1. Swallowing exceptions to keep running

What happens: catch (Exception) { } or log-and-continue everywhere. Why it is wrong: bad state propagates; the real failure appears later, far from its cause. Do instead: catch only what you can handle; let the rest reach a global handler.

2. Validating configuration lazily

What happens: options read and checked on first use. Why it is wrong: the app looks healthy until a user hits the path. Do instead: ValidateOnStart() so deployments fail before taking traffic.

3. Treating transient faults like bugs

What happens: a timeout to a partner API crashes the request pipeline or process. Why it is wrong: the system becomes fragile under normal partial failures. Do instead: retries, timeouts, circuit breakers and degradation at boundaries.

4. Environment.FailFast for ordinary errors

What happens: used for validation or I/O failures. Why it is wrong: skips finally and graceful shutdown; turns small errors into outages. Do instead: reserve it for state that's unsafe to continue with.

5. Guard clauses deep inside private methods only

What happens: public entry points accept anything; checks happen layers later. Why it is wrong: errors surface far from the caller's mistake. Do instead: guard at public boundaries and constructors.

6. Ignoring nullable warnings

What happens: #nullable disable or ! sprinkled to silence warnings. Why it is wrong: gives up compile-time fail fast. Do instead: fix the nullability; treat warnings as errors in new code.

7. Disabling DI validation in production "for speed"

What happens: ValidateOnBuild only in Development. Why it is wrong: missing registrations surface at runtime on specific paths. Do instead: enable it in all environments (startup cost is small) or test the container in CI.

8. Returning null for invalid input

What happens: CreateCustomer("") returns null. Why it is wrong: the problem moves to whoever dereferences it later. Do instead: throw, or return an explicit result type.

9. Health checks that don't reflect readiness

What happens: /health returns 200 even when critical config is broken. Why it is wrong: orchestrators route traffic to broken instances. Do instead: readiness checks that depend on successful startup validation and critical dependencies.

10. Crashing whole batches on one bad record

What happens: a nightly import stops at the first invalid row. Why it is wrong: fail fast applied at the wrong granularity. Do instead: fail fast per item (reject/dead-letter it), keep processing, and alert.

🏛 Senior insight: trade-offs

I distinguish failures by cause: programming and configuration errors should stop as early as possible; environmental errors at boundaries should be handled with resilience. Getting this split right is what makes systems both safe and available.

Failure Response
Missing or invalid configuration Fail at startup, block deployment
Missing DI registration ValidateOnBuild
Invalid argument from a caller Guard clause, ArgumentException
Transient HTTP error Retry with backoff (Microsoft.Extensions.Http.Resilience)
Dependency down for long Circuit breaker, degrade
Corrupted critical state Environment.FailFast

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions throw helpers and the Try pattern Why: Probes API conventions and least astonishment
    How to answer

    The principle of least astonishment says code should behave the way a reasonable user expects from its name, signature and context. In C# that means following .NET conventions: a TryParse method returns bool and never throws for bad input, an ...Async method returns a task and accepts a CancellationToken as its last parameter, properties are cheap and have no side effects, and Equals, GetHashCode and == agree. Surprises such as a GetUser that creates a user, or a property that makes a network call, cause bugs because callers rely on the convention rather than reading the implementation.

  2. When: Candidate validates configuration at startup Why: Links to configuration conventions
    How to answer

    Convention over configuration means a framework assumes sensible defaults, so you only write configuration where your case differs. EF Core is the clearest .NET example: a property called Id or OrderId becomes the primary key, navigation properties create relationships and foreign keys, and table names come from DbSet names. ASP.NET Core loads appsettings.{Environment}.json automatically, maps __ in environment variables to : in configuration keys, and finds controllers by naming. It removes boilerplate and makes projects consistent, but conventions are invisible, so I document team conventions, use ConfigureConventions for project-wide rules, and override explicitly where the default is wrong.

  3. When: Candidate mentions ValidateOnBuild and scopes Why: Checks DI and dependency inversion knowledge
    How to answer

    DIP says high-level policy should not depend on low-level details; both should depend on abstractions, and the abstraction belongs to the high-level side. For example, the domain or application layer defines IPaymentGateway, and the infrastructure project implements it with Stripe; the arrow of source dependency points inwards. Dependency injection is a technique for supplying those implementations (constructor injection), and the .NET DI container is a tool that automates it. You can use a container without inverting anything (if interfaces live next to their implementations in infrastructure), and you can invert dependencies without a container.

🛠 Real-world problems: how you handle them

Payment config error found by the first customer
Situation
A [checkout service] deployed with a mistyped payment configuration key.
Signals
Health checks green; first payment failed [40] minutes after deployment with a null URI error.
Investigation
Options were bound but never validated; the error appeared only when the gateway client was created.
Resolution
Added data annotations and `ValidateOnStart()`; enabled `ValidateOnBuild`.
Prevention
Readiness probe depends on successful startup; config schema checked in CI.

How to tell it (STAR)

We deployed with a typo in a payment setting and nothing noticed until a customer tried to pay [40] minutes later. I added options validation that runs at startup, so a bad configuration now stops the app before it takes traffic. The next mistake failed the rollout in seconds instead of reaching customers.

↩ Usually asked after

More in Additional Principles

Last updated 31 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me