The bug was in production for eleven months before anyone found it.
A payments platform had an authorization check:
if not user.is_suspended or user.has_override:
process_payment(user, amount)The intent was: “process if the user is not suspended, OR if they have a manual override.” The code reads like that intent. But it does not implement it.
The correct logic is: “process if (not suspended) OR (has override).”
The code reads not user.is_suspended or user.has_override,
which, by operator precedence, evaluates as
(not user.is_suspended) or (user.has_override).
Those are the same thing. So where is the bug?
The bug was in a different function — one that computed
is_suspended by combining two flags:
# Original intent: suspended if account_frozen AND payment_blocked
user.is_suspended = account_frozen and payment_blockedA developer needed to change the policy: “suspended if account_frozen OR payment_blocked.” They updated the assignment:
user.is_suspended = account_frozen or payment_blockedThen the authorization check, which negated
is_suspended, silently changed meaning. The original check
read: “not (frozen AND blocked).” By De Morgan’s Law, this equals “(not
frozen) OR (not blocked)”, which is True for almost every user. The
intent was never this permissive. Accounts that were frozen but not
blocked could process payments.
Eleven months. The fix was one line. The damage was regulatory.
De Morgan’s Laws are not an academic curiosity. They are the source of real authorization failures.
Thread 7 (State Machines) continues here from Chapter 1.
Chapter 1 established the atomic unit — a single statement with a
truth value. This chapter adds the connectives that combine atomic
statements into compound ones. Compound statements are the building
blocks of compound state: a system that is simultaneously in multiple
conditions. A request is
authenticated AND authorized AND rate_not_exceeded. A
distributed node is
leader AND quorum_reached AND log_committed. Every compound
state in every system you will build is a logical expression over atomic
propositions.
The direction is: atomic propositions (Ch 1) → compound state (this chapter) → compound state in state machines (Ch 13) → stateless design in distributed services (Book 3, Ch 3).
The backward link: this chapter also activates Thread 12 (Tradeoffs). The exponential complexity introduced by combining boolean variables — the subject of Chapter 3 — is a direct consequence of the combinatorial space opened by logical operators.
Logical operators combine statements into new statements. The four fundamental operators are:
| Operator | Symbol | Name | Evaluates to True when… |
|---|---|---|---|
| AND | ∧ | Conjunction | Both operands are True |
| OR | ∨ | Disjunction | At least one operand is True |
| NOT | ¬ | Negation | The operand is False |
| XOR | ⊕ | Exclusive Or | Exactly one operand is True |
These are complete definitions. There is no ambiguity. Each operator is fully defined by its truth table — a table listing every combination of input values and the resulting output. Truth tables are Chapter 3’s subject. For now, the definitions above are sufficient.
P AND Q is True if and only if both P is True and Q is
True. One False operand makes the whole expression False.
In programming:
if user.is_authenticated and user.has_permission("write"):
— access is granted only when both conditions hold.
P OR Q is True if at least one of P, Q is True. Both can
be True simultaneously. This is inclusive OR — it
includes the case where both are True.
In programming:
if request.is_cached or fallback_available: — service
continues if either condition holds.
NOT is unary — it takes one operand and inverts its truth value. True becomes False; False becomes True.
In programming: if not user.is_banned: — the inverse of
the stored condition.
XOR is True when exactly one operand is True. It is False when both are True and False when both are False. XOR is less common in high-level boolean logic, but it appears in:
state = state XOR True flips a boolean
without an if-statement# XOR implemented in Python (Python has no boolean XOR keyword, use !=)
def xor(p: bool, q: bool) -> bool:
# XOR: exactly one of p, q must be True
return p != q
# Alternatively, explicit:
def xor_explicit(p: bool, q: bool) -> bool:
return (p or q) and not (p and q)De Morgan’s Laws are the most important identities in boolean algebra for working programmers.
Law 1: NOT (P AND Q) =
(NOT P) OR (NOT Q)
Law 2: NOT (P OR Q) =
(NOT P) AND (NOT Q)
In plain language:
The transformation is symmetric. If you negate the result of applying the law, you get back the original.
Why this matters in code:
Programmers frequently write conditions as negative checks. “Don’t allow if suspended or banned.” In code:
# Version A: direct negation of compound condition
if not (user.is_suspended or user.is_banned):
allow_access()
# Version B: De Morgan's applied manually
if not user.is_suspended and not user.is_banned:
allow_access()These are equivalent. A and B evaluate identically for every
combination of is_suspended and is_banned. You
can verify this with a truth table (Chapter 3 will formalize this). For
now, trust the law and verify with a few cases:
| is_suspended | is_banned | A result | B result |
|---|---|---|---|
| False | False | True | True |
| True | False | False | False |
| False | True | False | False |
| True | True | False | False |
Identical. The laws are correct.
Where De Morgan’s fails programmers:
The failure is not in the law. The law is provably correct. The failure is in applying the law incorrectly while refactoring.
Consider the payments bug from the opening hook. The original condition was computed as:
is_suspended = account_frozen and payment_blocked # True only when BOTH holdThe authorization check was:
if not is_suspended: # allow if NOT (frozen AND blocked)
# By De Morgan: allow if (NOT frozen) OR (NOT blocked)
# This is True for almost everyone
process_payment()The developer changed is_suspended to
account_frozen or payment_blocked, intending to catch
either condition. Now:
is_suspended = account_frozen or payment_blocked # True if EITHER holdsBut the authorization check still reads
not is_suspended, which now means
NOT (frozen OR blocked), which by De Morgan’s equals
(NOT frozen) AND (NOT blocked). This is stricter than
before: only accounts that are neither frozen nor blocked can process
payments. That stricter behavior may well be what the developer intended
for the new policy, but it is not what callers expected. Code written
against the old definition of is_suspended (AND semantics,
permissive) now silently operates under the new definition (OR
semantics, strict). The real failure is that the intermediate variable
changed meaning without any visible signal — its name, type, and call
site all remained identical. The contract (FM8) broke without an error
being raised. Any system that depended on the old behavior — including
callers that deliberately relied on frozen-but-not-blocked accounts
being allowed through — now behaves differently, silently and
immediately.
# Safer pattern: name the authorization condition explicitly
def can_process_payment(account_frozen: bool, payment_blocked: bool) -> bool:
# Explicit statement: account is usable for payments
# Named conditions prevent silent semantic drift
account_is_usable = not account_frozen and not payment_blocked
return account_is_usableBy naming the terminal condition, you eliminate the intermediate
is_suspended variable and the implicit De Morgan
inversion.
Most programming languages implement short-circuit evaluation for AND and OR. This is a performance and safety optimization derived from the truth table structure.
Short-circuit AND:
If the left operand of P AND Q is False, the whole
expression is False. The right operand does not need to be evaluated.
Python, Java, C, JavaScript — all short-circuit AND.
def get_user_role(user_id: int, db) -> str | None:
# Short-circuit AND: if user_id is falsy, db.get() is never called
# This prevents a database call with an invalid ID
user = user_id and db.get_user(user_id)
return user.role if user else None
# More common pattern: guard clause using short-circuit
def process(items: list, transform) -> list:
# If items is empty, transform is never called
return items and [transform(item) for item in items]Short-circuit OR:
If the left operand of P OR Q is True, the whole
expression is True. The right operand is not evaluated.
def get_config_value(key: str) -> str:
# Short-circuit OR: use cached value if available, otherwise fetch
# fetch_from_remote() is only called if cache returns None/falsy
return config_cache.get(key) or fetch_from_remote(key)Implications for side effects:
Short-circuit evaluation means the right operand might not run. If the right operand has side effects — increments a counter, logs a message, modifies state — those side effects are conditional on the left operand’s value.
# Dangerous: side effect in right operand
# record_attempt() may or may not run depending on is_enabled
if is_enabled and record_attempt(user_id):
grant_access()If is_enabled is False, record_attempt is
never called. If audit logging is required for all access attempts
(including denied ones), this code silently skips the log. This is FM11
(Observability Blindness) in miniature — the system fails to record
events it should be recording.
Non-short-circuit evaluation:
When you need both operands to evaluate regardless of the first, avoid compound boolean expressions with side-effectful operands. Call each function separately and store the result:
# Safe: both functions always called
attempt_recorded = record_attempt(user_id) # always runs
is_enabled = check_enabled(user_id) # always runs
if is_enabled and attempt_recorded:
grant_access()In code, NOT binds tighter than AND, which binds tighter than OR. This matches mathematical convention.
NOT > AND > OR
not a or b and c parses as
(not a) or (b and c).
Explicit parentheses eliminate ambiguity. The recommendation is simple: parenthesize compound conditions. The performance cost is zero. The readability cost is zero. The correctness benefit is real.
# Ambiguous: relies on precedence knowledge
if not is_suspended or has_override and is_premium:
...
# Unambiguous: explicit structure
if (not is_suspended) or (has_override and is_premium):
...Each operator’s behavior is fully captured by a 2-variable truth table. Chapter 3 builds these systematically. For reference:
P Q P AND Q P OR Q NOT P P XOR Q
True True True True False False
True False False True False True
False True False True True True
False False False False True False
AT3 — Simplicity vs. Flexibility
Four operators: AND, OR, NOT, XOR. This is a minimal, complete set. In fact, AND and NOT alone are functionally complete — every possible boolean function can be expressed using only AND and NOT. NAND (NOT AND) is also functionally complete by itself. Hardware designers use NAND gates extensively because NAND is cheap to fabricate and expressive enough to build everything else.
The simplicity is the point. Four operators. Fixed truth tables. No ambiguity. The cost of this simplicity is that complex real-world conditions require verbose expressions.
“Allow access if the user is an admin, or if they own the resource and either the resource is public or they have an explicit grant, but not if their account is suspended regardless of ownership.”
This translates to:
def can_access(user: User, resource: Resource) -> bool:
# Atomic statements
is_admin = user.role == "admin"
owns_resource = resource.owner_id == user.id
resource_is_public = resource.visibility == "public"
has_explicit_grant = user.id in resource.explicit_grants
is_suspended = user.account_status == "suspended"
# Compound statement: access rule
base_access = is_admin or (owns_resource and (resource_is_public or has_explicit_grant))
# Override: suspension blocks all access
return base_access and not is_suspendedThe verbosity is intentional. Each named variable is an atomic statement. The compound statement is built from named parts. The structure is readable, testable, and debuggable. The alternative — one long inline expression — is flexible (fewer lines) but less simple to reason about. AT3 applies: the named-variable approach favors simplicity of reasoning; the inline approach favors brevity.
FM4 — Data Consistency Failure
Misapplying De Morgan’s Laws causes inconsistent boolean evaluation across system boundaries.
The canonical failure: a server enforces an access rule using one boolean expression. A client enforces the same rule using a supposedly equivalent expression. The expressions are not actually equivalent — De Morgan’s was applied incorrectly. The server allows requests the client thinks it blocked. The inconsistency is undetected until a security audit.
# Server: allow if not (read_only and no_write_grant)
# Intent: "block only if BOTH conditions hold"
server_allows = not (user.is_read_only and not user.has_write_grant)
# By De Morgan: (not is_read_only) OR (has_write_grant)
# Client: blocks if read_only OR no write grant
# Intent: "block if EITHER condition holds"
client_blocks = user.is_read_only or not user.has_write_grant
# client_allows = not client_blocks = (not read_only) AND (has_write_grant)server_allows and client_allows are
different. A user who is read-only but has a write grant is allowed by
the server (because has_write_grant is True, the OR fires)
but blocked by the client (because is_read_only is True,
the AND fails). The server and client disagree. Requests that reach the
server get processed. The client never sends them. Or worse: a different
client sends them directly.
The prevention is shared predicate functions. Define the access rule once. Call it from both sides.
# Shared predicate: single source of truth for the access rule
def user_can_write(user: User) -> bool:
# Explicit statement: user is permitted to write
return not user.is_read_only or user.has_write_grant
# Server and client both call user_can_write(user)
# They cannot disagree because they call the same functionAccess Control Lists — UNIX file permissions are
AND/OR/NOT over user, group, and other permission bits.
chmod 755 sets specific bits; ls -l reads them
back as boolean flags. The execute bit for a directory
means “can traverse” — a compound access decision ANDs the traverse bit
with the parent directory’s bits recursively.
Kubernetes Pod Scheduling — a Pod’s scheduling
constraints are boolean expressions. nodeAffinity
expressions use AND/OR to combine requirements. A node must match all
required terms (AND) and any of the preferred terms (OR). The scheduler
evaluates these expressions for every candidate node. An incorrectly
specified affinity expression causes Pods to be scheduled to wrong nodes
or not scheduled at all.
Firewall Rules — iptables and cloud security groups evaluate packets against chains of rules, each of which is a conjunction (AND) of match criteria: source IP AND destination port AND protocol. The rule matches only when all criteria match. De Morgan’s appears when rules are negated: “block all traffic except from 10.0.0.0/8” is implemented as two rules because negation distributes differently over sets of criteria.
Circuit Breakers — a circuit breaker opens (stops
traffic) when
failure_rate > threshold AND window_is_active. It resets
(allows traffic) when
probe_succeeds OR reset_timeout_expired. These are literal
boolean expressions evaluated by the circuit breaker at each request.
Short-circuit evaluation applies: if
failure_rate > threshold is False,
window_is_active may not be checked.
SQL WHERE Clauses — every SQL WHERE
clause is a boolean expression over column predicates. The query
optimizer applies De Morgan’s Laws and boolean identities to rewrite the
expression into a form that can use indexes efficiently.
WHERE NOT (a = 1 AND b = 2) may be rewritten as
WHERE a != 1 OR b != 2 if that form better exploits
available indexes. The optimizer is an automated De Morgan’s
transformer.
Concept: Logical Operators (AND, OR, NOT, XOR) and De Morgan’s Laws
Core Idea: AND, OR, NOT, and XOR combine atomic statements into compound ones. De Morgan’s Laws define how negation distributes over AND and OR — misapplying them is a consistent source of authorization bugs.
Tradeoff: AT3 — Simplicity vs. Flexibility (four operators are minimally sufficient but verbose for complex conditions; named atomic variables trade brevity for clarity)
Failure Mode: FM4 — Data Consistency Failure (two components applying De Morgan’s independently can evaluate the same rule differently)
Signal: When an authorization check produces unexpected results, or when a NOT distributes over a compound condition during refactoring — check De Morgan’s. Draw the truth table.
NOT (user_is_admin OR user_is_owner)NOT (request_is_valid AND session_is_active AND rate_not_exceeded)NOT (NOT a AND NOT b)P = True and Q = False, evaluate:
P AND QP OR QNOT P OR QNOT (P AND NOT Q)P XOR QA rate limiter has this condition:
def should_allow(request_count: int, limit: int, has_premium: bool, is_admin: bool) -> bool:
return (request_count < limit or has_premium) and not is_adminThe developer meant: “allow if under limit or premium, but always
block admins (they use a separate endpoint).” Is the code correct?
Evaluate for request_count=100, limit=50,
has_premium=False, is_admin=False.
A new requirement: “never block if admin.” The developer changes
not is_admin to is_admin. Evaluate the same
inputs. What changed?
The actual requirement is: “allow if under limit OR has_premium, regardless of admin status — admin is handled elsewhere.” Rewrite the function to match this intent and remove the admin check entirely. Name the AT3 tradeoff involved.
An e-commerce platform has this access rule, stated in English:
“A user can modify an order if they placed the order and the order is not yet shipped, OR if they are a support agent and the order has an open dispute, OR if they are an admin.”
Identify every atomic statement in this rule.
Write a Python function
can_modify_order(user, order) -> bool using named
boolean variables for each atomic statement.
Write the De Morgan’s-equivalent formulation of the first clause only: “(placed the order) AND (not yet shipped)”. What does the De Morgan’s form express in plain language? Is it useful?
Suppose the rule is enforced on both the frontend (React) and backend (Python API). Identify two specific ways FM4 (Data Consistency Failure) can occur, and propose a mitigation that uses the AT8 (Coupling vs. Cohesion) tradeoff explicitly.
A complete answer will: define the chosen approach, quantify its cost or complexity, and identify what breaks first as scale or load increases.