Zoran Horvat’s Advanced Defensive Programming Techniques starts from a provocative idea: much defensive code exists because the design allows states that should never have been representable in the first place.

Pluralsight currently describes the course as a progression from explicit defensive coding toward defensive design. It moves through consistent object construction, valid state transitions, primitive obsession, function domains, null handling, rich domain models, and alternative error workflows.

After reviewing the nine modules and the C# demos, the conclusion is interesting: the central thesis has aged very well; some of the code has not.

In fact, several examples contain exactly the kind of bugs the course is trying to teach us to prevent.

This article is deliberately different from our earlier analysis of Horvat’s ideas inside the OpenAI Codex codebase. Here the goal is to review the complete course, identify what still matters in 2026, what should be reinterpreted, and which implementation details should not be copied.

The thesis: move defense into the design

The course repeatedly uses the line:

“When you have to defend, you have already lost.”

Taken literally, that would be too absolute. As a design heuristic, it is excellent.

It does not mean that JSON, user input, HTTP responses, or configuration should not be validated. It means that once data has crossed a system boundary, every internal method should not have to rediscover the same invariants.

The pattern is:

UNTRUSTED INPUT
       |
       v
parse / validate / normalize
       |
       v
DOMAIN TYPES
       |
       v
CORE WITH INVARIANTS
       |
       v
OUTPUT

The smaller the area in which data is uncertain, the less defensive code needs to be distributed throughout the program.

The course as a progression of constraints

The nine modules can be understood as one gradual transformation.

StepProblemDesign response
1repeated if, try/catch and guardsrecognize the limits of defensive coding
2objects born invalidconstruct only consistent objects
3mutations that break invariantsallow only valid transitions
4primitive obsessionintroduce domain types
5functions accept too muchreduce their domain
6rules are scatteredencapsulate behavior
7null is implicit statemake absence explicit
8anemic modelsenrich the domain
9exceptions represent normal outcomesmodel alternative workflows

The accumulated idea looks like this:

Primitive
   |
   v
Value Object
   |
   v
Valid Object
   |
   v
Valid State Machine
   |
   v
Restricted Function Domain
   |
   v
Encapsulated Behavior
   |
   v
Explicit Optionality
   |
   v
Explicit Success / Failure

Each step moves knowledge away from procedural checks and into program structure.

1. Make invalid states harder to represent

Imagine:

void Deposit(decimal amount)
{
    if (amount <= 0)
        throw new ArgumentException();
}

The function accepts every decimal value although its actual domain is much smaller.

A different design would conceptually be:

void Deposit(PositiveMoney amount)
{
    // amount already belongs to the permitted domain
}

The validation does not disappear. It moves to the point at which PositiveMoney is created.

That matters because one validation at a boundary can replace dozens of internal guards.

It is a practical form of the familiar principle:

Make illegal states unrepresentable.

C# cannot always achieve this perfectly, but small types, private constructors, factories, and APIs that do not expose invalid intermediate states can get us much closer.

2. Primitive obsession remains one of the strongest lessons

A method such as:

CreateUser(
    string email,
    string country,
    decimal amount)

accepts syntactically convenient but semantically enormous types.

The compiler does not know that:

  • email must be a valid address;
  • country must belong to a defined set;
  • amount must satisfy monetary rules.

A more expressive interface is:

CreateUser(
    EmailAddress email,
    CountryCode country,
    PositiveMoney amount)

The change looks small but reshapes the architecture.

Before, any caller can inject nonsense and every layer must defend itself.

Afterwards, the system can establish a boundary:

string
  |
  v
EmailAddress.TryCreate(...)
  |
  +---- error
  |
  v
EmailAddress

Inside the core we no longer work with an arbitrary string.

3. Function domains are a defensive mechanism

One of the best ideas in the course is to look at functions mathematically.

If an operation only makes sense for positive integers, then:

Process(int value)

lies about its domain.

The signature permits values that conceptually do not belong to the operation.

That design error later produces:

if (value <= 0)
    ...

Horvat turns the question around: make the signature narrower.

This is particularly useful in code review. A strong question is:

Is this guard clause protecting a real boundary, or compensating for an overly permissive type?

If it validates external input, it is probably in the right place.

If ten internal methods verify that the same identifier is not empty, a type is probably missing.

4. Valid construction and valid transitions are different problems

An object can be born valid and become corrupt later.

The course therefore separates two questions:

  1. Can I construct an invalid instance?
  2. Can I turn a valid instance into an invalid one?

The second question leads naturally to state machines and constrained mutability.

Instead of allowing arbitrary movement:

A <-> B <-> C <-> D

we can restrict the path:

A -> B -> C -> D

This is constrained mutability: the object may change, but only through operations that preserve its invariants.

The idea remains useful in workflows, orders, payments, pipelines, and agents.

Execution state should not be an accidental collection of booleans:

bool Started;
bool Finished;
bool Failed;
bool Retrying;

because meaningless combinations appear.

Explicit alternatives are safer.

5. Option and Either: good ideas, but modern C# offers more

The course devotes one module to replacing null with a custom Option and another to representing alternative outcomes with an Either type.

Conceptually, both ideas remain solid:

Option<T>
  = Some(T)
  | None

Result<T, E>
  = Success(T)
  | Failure(E)

Absence and failure stop hiding inside implicit control flow.

But C# has changed considerably.

Microsoft now documents Nullable Reference Types as a static-analysis feature that lets developers express intent through string versus string? and receive compiler diagnostics for possible null dereferences.

That greatly reduces the need to invent Option for every optional reference.

Option still has value when absence is part of the domain semantics, rather than merely a technique for avoiding NullReferenceException.

The same modernization applies to records and record structs. C# generates value equality for records, removing a significant amount of manual ceremony that older examples had to implement themselves.

Four concrete problems in the demos

The material becomes even more useful here because its imperfections show how difficult these ideas are to apply correctly.

Bug 1: Grade looks like a value object but keeps reference equality

One demo turns grades into a class resembling:

public class Grade
{
    private Grade(double numericEquivalent)
    {
        NumericEquivalent = numericEquivalent;
    }

    public static Grade A => new Grade(4);
    public static Grade B => new Grade(3);
    public static Grade F => new Grade(0);
}

Later, business logic compares a stored grade with Grade.F.

The problem is that every access to Grade.F creates a new instance.

Without correct Equals and GetHashCode semantics, a comparison such as:

Grade.F == Grade.F

compares references rather than values.

Two objects that conceptually mean the same grade can appear different.

Modern C# gives us safer building blocks such as record and readonly record struct, provided we still control which values can be constructed.

public readonly record struct Grade
{
    public int Value { get; }

    private Grade(int value) => Value = value;

    public static Grade A => new(4);
    public static Grade B => new(3);
    public static Grade F => new(0);
}

The lesson is not “always use records.”

The lesson is that a value object needs complete value semantics, not merely a class with a private constructor.

Bug 2: PersonalName breaks the Equals/GetHashCode contract

Another demo compares names without regard to case but computes hashes in a way that may not use exactly the same semantics.

That can create:

a.Equals(b) == true

but

a.GetHashCode() != b.GetHashCode()

This contract matters for Dictionary and HashSet.

The bug is particularly instructive in a defensive-design course: wrapping a string in a type is not enough. The type must correctly preserve every law it promises.

Modern C# code should use the same StringComparer for equality and hashing.

private static readonly StringComparer Comparer =
    StringComparer.OrdinalIgnoreCase;

public bool Equals(PersonalName? other) =>
    other is not null &&
    Comparer.Equals(FirstName, other.FirstName) &&
    Comparer.Equals(LastName, other.LastName);

public override int GetHashCode() =>
    HashCode.Combine(
        Comparer.GetHashCode(FirstName),
        Comparer.GetHashCode(LastName));

Bug 3: Option.Some(null) reintroduces the state we were removing

The demo Option permits the conceptual equivalent of:

Option.Some<string>(null)

That leaves three states:

null
None<string>
Some<string>(null)

Instead of simplifying the model, we made it more complicated.

If we create a custom Option type, Some must guarantee that it contains a value valid under that Option’s rules.

The broader lesson is:

A wrapper does not create an invariant simply by existing.

The invariant exists when every construction path preserves it.

Bug 4: internal APIs can bypass the original guarantees

A design may have a safe public constructor and still reintroduce corruption through factories, copy methods, or internal helpers.

For example, an operation such as:

application.With(grade)

can reintroduce Some(null) if it does not obey exactly the same rules as initial construction.

This is a common failure mode in real domain models.

Auditing the constructor is not enough.

We must audit:

constructor
factory
builder
With(...)
copy
deserialization
ORM hydration
mapping
event replay

Every path that can produce an instance is part of the consistency boundary.

What has aged in the code

The demos target an older .NET stack: .NET Framework 4.5.2, classic ASP.NET MVC, and APIs such as WebRequest.

I would not use that code as a modern application template.

But separating ideas from mechanisms is exactly what makes the course useful.

Current C# makes the original philosophy easier to express:

  • Nullable Reference Types;
  • records and record structs;
  • init-only properties;
  • required members;
  • pattern matching;
  • switch expressions;
  • stronger analyzers;
  • modern HTTP and persistence APIs.

Microsoft describes records as types with compiler-generated value equality, while Nullable Reference Types let code express nullability intent and receive compiler diagnostics.

In other words, the language can now perform a larger share of the defensive work that older code had to enforce through conventions.

What should not become dogma

The course uses deliberately strong formulations. They are useful pedagogically but should not become universal laws.

“No exceptions”

A more practical distinction is:

expected domain outcome
        |
        v
Result / Either / union

exceptional failure, bug or infrastructure
        |
        v
Exception

“Card declined” can be a normal outcome.

“An internal stream is corrupt in a way that violates our invariants” may still be exceptional.

“Never null”

With Nullable Reference Types, null can be a perfectly explicit representation of absence when an API communicates it correctly.

The problem is not the four letters n-u-l-l.

The problem is implicit or poorly modeled absence.

“One constructor”

Sometimes one construction path makes invariants easier to reason about. In other domains, several clearly named factories improve the contract.

The useful rule is:

Every construction path must produce a valid object.

The advanced part: history instead of destructive mutation

The rich-domain-model module eventually approaches historical modeling and event sourcing.

Instead of treating current state as the only truth:

State = D

we can treat it as the consequence of facts:

E1 -> E2 -> E3 -> E4
              |
              v
           State D

This is powerful because facts can be append-only.

I would not adopt Event Sourcing merely to remove defensive code. It has real costs: storage, event versioning, projections, replay, observability, and operations.

But the intuition of monotonicity is broadly useful.

When an operation represents an irreversible domain fact, modeling it as something that happened is often safer than allowing arbitrary edits to previous state.

How I would apply the course today

For a modern project, I would use this sequence:

1. Validate at boundaries
   HTTP / CLI / queue / DB / external APIs

2. Convert primitives into domain types
   EmailAddress, OrderId, Money, Percentage...

3. Construct entities only from consistent states

4. Model workflows as explicit states

5. Make nullable only what can semantically be absent

6. Represent expected outcomes explicitly

7. Reserve exceptions for genuinely exceptional conditions

8. Test invariants, not only examples

The last point is particularly important.

These ideas work extremely well with property-based testing.

If we say:

Every valid Grade has Value between 0 and 4

that is a property.

If we say:

After OrderId is created, it is never empty

that is another.

If we say:

The transition Completed -> Processing does not exist

that is another property.

Defensive design becomes much stronger when its invariants can be executed as tests.

A code-review heuristic

Whenever a guard clause appears, do not remove it automatically.

Ask:

  1. Am I at an external boundary?
  2. Does this condition represent a domain invariant?
  3. Could a type represent that invariant?
  4. Could construction guarantee it once?
  5. Can the object violate it later?
  6. Is there another construction path that bypasses it?

If the answers point toward the model, improving the design is usually more valuable than adding another if.

Conclusion

Advanced Defensive Programming Techniques remains valuable because it changes the question.

Instead of asking:

Where should I add another check?

ask:

Why can this invalid state be represented here?

That shift leads naturally to value objects, constructors that establish invariants, state machines, explicit optionality, and typed results.

The course code needs modernization and contains several implementation bugs worth avoiding. But that does not weaken its thesis; it reinforces it.

Defensive design does not mean hiding errors. It means systematically reducing the number of incorrect states the program is capable of constructing.

And in 2026, C# is significantly better equipped to do exactly that.

References