In Advanced Defensive Programming Techniques, Zoran Horvat develops a useful and slightly uncomfortable idea: the best defensive code is often the code you can remove because the design prevents invalid state from reaching that point in the first place.
The course frames the idea with a blunt line: “When you have to defend, you have already lost.” This does not mean that external input should not be validated. It means that repeatedly defending the same invariant inside a system is often evidence that the invariant is missing from the model.
The public OpenAI Codex repository contains several strong examples of the same philosophy in Rust. There is no evidence that the Codex team was explicitly following Horvat’s course; this is a conceptual comparison. But the patterns align remarkably well with modules 2, 3, and 4.
Pluralsight describes the course as a progression from explicit defensive coding toward defensive design. Those three modules move through exactly that progression:
- understand the limitations of traditional defensive code;
- create only consistent objects;
- allow only valid state transitions.
Here is what that looks like in a production-grade open-source codebase.
1. Module 2: restrict the domain before the function runs
A traditional defensive approach often accepts a type that is broader than the real domain and rejects invalid values inside each function.
For example:
fn connect(protocol_version: u32) {
if protocol_version == 0 {
// error
}
// ...
}
The problem is not the if. The problem is that the function accepts a u32 even though the real domain is “a positive non-zero integer.”
Codex models that domain directly:
pub struct ProtocolVersion(NonZeroU32);
impl ProtocolVersion {
pub const V1: Self = Self(NonZeroU32::MIN);
pub const fn new(value: u32) -> Option<Self> {
match NonZeroU32::new(value) {
Some(value) => Some(Self(value)),
None => None,
}
}
pub const fn get(self) -> u32 {
self.0.get()
}
}
Source: codex-rs/code-mode-protocol/src/host/types.rs.
A function receiving ProtocolVersion no longer needs to ask whether the value is zero. Zero cannot be represented by the type.
Conceptually:
u32
┌─────────────────────────┐
│ 0 1 2 3 4 ... │
└─────────────────────────┘
│
│ ProtocolVersion::new
▼
ProtocolVersion
┌─────────────────────────┐
│ 1 2 3 4 ... │
└─────────────────────────┘
Validation happens once at the boundary where raw data becomes a domain type. The rest of the program gets a stronger guarantee.
NonEmptyString: another explicit boundary
In the same file, Codex defines:
struct NonEmptyString(String);
impl NonEmptyString {
fn new(value: impl Into<String>) -> Result<Self, InvalidIdentifier> {
let value = value.into();
if value.trim().is_empty() {
Err(InvalidIdentifier)
} else {
Ok(Self(value))
}
}
}
Then it reuses that invariant:
pub struct Capability(NonEmptyString);
pub struct SessionId(NonEmptyString);
A function receiving a SessionId does not have to repeat:
if id.is_empty() { ... }
if id.trim().is_empty() { ... }
The invariant is part of the type.
This is a central Horvat idea: a function’s domain should express which values are actually acceptable, not merely which primitive representation is convenient.
2. Module 3: create only consistent objects
Restricting individual values helps, but an object can still be invalid because of the combination of several values.
The next step is stronger: if an instance exists, it should satisfy its invariants.
Codex has a particularly clean example:
pub struct CapabilitySet(BTreeSet<Capability>);
impl CapabilitySet {
pub fn try_new(
capabilities: impl IntoIterator<Item = Capability>,
) -> Result<Self, DuplicateCapability> {
let mut unique = BTreeSet::new();
for capability in capabilities {
if !unique.insert(capability.clone()) {
return Err(DuplicateCapability { capability });
}
}
Ok(Self(unique))
}
}
The important detail is that the internal BTreeSet is encapsulated. Consumers do not receive an arbitrary collection and then have to remember to check duplicates. They must go through the construction operation.
The flow becomes:
potentially inconsistent data
│
▼
try_new
/ \
/ \
error valid object
│
▼
CapabilitySet
After construction, the rest of the system can work with a guarantee.
Multiple invariants in one type
SupportedProtocolVersions goes further:
pub struct SupportedProtocolVersions(BTreeSet<ProtocolVersion>);
impl SupportedProtocolVersions {
pub fn try_new(
versions: impl IntoIterator<Item = ProtocolVersion>,
) -> Result<Self, InvalidSupportedProtocolVersions> {
let mut unique = BTreeSet::new();
for version in versions {
if !unique.insert(version) {
return Err(
InvalidSupportedProtocolVersions::Duplicate(version)
);
}
}
if unique.is_empty() {
return Err(InvalidSupportedProtocolVersions::Empty);
}
Ok(Self(unique))
}
}
The constructor establishes at least two invariants:
- versions are unique;
- the set is not empty.
So:
[] → invalid
[1, 1] → invalid
[1, 2] → valid
Once a SupportedProtocolVersions exists, downstream code should not need an if versions.is_empty() to protect itself from an object that should never have existed.
PluginStore: path-based consistency
The same pattern appears in PluginStore:
pub struct PluginStore {
codex_home: AbsolutePathBuf,
root: AbsolutePathBuf,
data_root: AbsolutePathBuf,
}
Its try_new converts and validates the paths before constructing the object:
pub fn try_new(codex_home: PathBuf) -> Result<Self, PluginStoreError> {
let root =
AbsolutePathBuf::from_absolute_path_checked(
codex_home.join(PLUGINS_CACHE_DIR)
)
.map_err(|err| {
PluginStoreError::io("failed to resolve plugin cache root", err)
})?;
let data_root =
AbsolutePathBuf::from_absolute_path_checked(
codex_home.join(PLUGINS_DATA_DIR)
)
.map_err(|err| {
PluginStoreError::io("failed to resolve plugin data root", err)
})?;
let codex_home =
AbsolutePathBuf::from_absolute_path_checked(codex_home)
.map_err(|err| {
PluginStoreError::io("failed to resolve Codex home", err)
})?;
Ok(Self {
codex_home,
root,
data_root,
})
}
Source: codex-rs/core-plugins/src/store.rs.
If you have a PluginStore, its roots are already AbsolutePathBuf. The design avoids forcing every method to ask again whether a path is absolute.
3. Module 4: model valid states, not combinations of flags
The third step appears when an object changes over time.
Imagine an event broker represented like this:
struct EventBroker {
paused: bool,
needs_start: bool,
source: Option<EventSource>,
}
That representation permits ambiguous combinations:
paused = true
needs_start = true
source = Some(...)
what state is this?
It might also permit:
paused = false
needs_start = false
source = None
The representation can express states that the domain may never intend to support.
Codex avoids this with an enum:
enum EventBrokerState<S: EventSource> {
Paused,
Start,
Running(S),
}
Source: codex-rs/tui/src/tui/event_stream.rs.
Now state can only be one of three alternatives:
Paused
│
│ resume
▼
Start
│
│ first poll
▼
Running(S)
│
│ pause
└──────────────→ Paused
And Running carries exactly the data that makes sense only in that state: S, the event source.
The Start → Running transition is centralized:
fn active_event_source_mut(&mut self) -> Option<&mut S> {
match self {
EventBrokerState::Paused => None,
EventBrokerState::Start => {
*self = EventBrokerState::Running(S::default());
match self {
EventBrokerState::Running(events) => Some(events),
EventBrokerState::Paused | EventBrokerState::Start =>
unreachable!(),
}
}
EventBrokerState::Running(events) => Some(events),
}
}
And public operations make explicit transitions:
pub fn pause_events(&self) {
// ...
*state = EventBrokerState::Paused;
}
pub fn resume_events(&self) {
// ...
*state = EventBrokerState::Start;
}
This removes an entire class of defensive checks whose only job would be reconciling flags and optional fields that contradict one another.
The full progression: from if statements to design
The three modules can be seen as one progression:
1. VALUES
restrict the domain
↓
ProtocolVersion(NonZeroU32)
NonEmptyString
2. OBJECTS
construct only consistent instances
↓
CapabilitySet::try_new()
SupportedProtocolVersions::try_new()
PluginStore::try_new()
3. STATE OVER TIME
represent only valid states
↓
EventBrokerState
├── Paused
├── Start
└── Running(S)
The underlying pattern is the same:
less:
if invalid { ... }
if invalid { ... }
if impossible { ... }
more:
types → construction → states → transitions
Validation does not disappear. It moves toward better-defined boundaries.
External data still needs parsing and validation. But once it becomes a strong internal type, every layer should not have to rediscover the same invariants.
Why Rust makes the idea unusually visible
These techniques do not belong to Rust. They apply to C#, Java, Kotlin, TypeScript, F#, Swift, and many other languages.
Rust simply makes them especially visible because it provides natural tools for expressing the model:
- newtypes such as
ProtocolVersion(NonZeroU32); Option<T>for explicit absence;Result<T, E>for fallible construction;- enums carrying associated data;
- exhaustive pattern matching;
- field privacy to preserve invariants.
The goal is not “more types” for aesthetic reasons. The goal is to move work from repeated runtime checks into the compiler and the structure of the program.
Applying the same criterion in C#
Although Horvat teaches many of these ideas from the .NET ecosystem, modern C# has increasingly good tools for expressing them.
Instead of:
void Connect(int protocolVersion)
{
if (protocolVersion <= 0)
throw new ArgumentOutOfRangeException();
}
we can introduce a value object:
public sealed record ProtocolVersion
{
public int Value { get; }
private ProtocolVersion(int value) => Value = value;
public static ProtocolVersion? TryCreate(int value) =>
value > 0 ? new ProtocolVersion(value) : null;
}
Invalid creation is represented as null at the boundary. The caller must resolve that possibility before entering domain code:
var version = ProtocolVersion.TryCreate(rawVersion);
if (version is null)
return;
Connect(version);
After that check, functions receiving a ProtocolVersion no longer need to defend against zero or negative integers. The reference type’s default state can still be null, but there is no ProtocolVersion instance with Value == 0.
And for mutually exclusive states, discriminated unions —native or modeled through closed hierarchies— can replace combinations of booleans and nullable fields with explicit alternatives.
A practical code-review rule
A useful review question is:
Is this guard clause protecting a real boundary, or compensating for an overly permissive model?
If it validates JSON, user input, configuration, or a remote response, it is probably at a legitimate boundary.
But if ten internal methods repeatedly check that the same identifier is non-empty, the same path is absolute, or the same two flags do not contradict each other, the answer may not be an eleventh if.
The system may need a better type.
Conclusion
The value of Horvat’s approach is not that defensive programming is forbidden. It is that defense moves to a more useful place.
Codex demonstrates three levels of that idea:
ProtocolVersionandNonEmptyStringnarrow the domain of values;CapabilitySet,SupportedProtocolVersions, andPluginStoremake construction establish invariants;EventBrokerStateturns implicit state combinations into explicit alternatives.
The result matters: many checks become unnecessary not because errors are ignored, but because the design makes those errors harder —or impossible— to represent.
That is the transition from defensive coding to defensive design.
References
- Zoran Horvat, Advanced Defensive Programming Techniques — Pluralsight.
- OpenAI, Codex — public repository.
- OpenAI Codex, protocol host types.
- OpenAI Codex, PluginStore.
- OpenAI Codex, TUI event state.