Java Defensive Programming
Purpose
Concentrate each defence at the boundary or state transition that owns its invariant, then remove
only checks proven redundant. The two failure
modes this skill prevents are opposites: the unguarded boundary that lets bad data deep
into the system before anything fails, and the codebase where every private method
re-checks every argument — noise that buries the checks that matter and asserts that
nobody knows where validation actually happened.
Workflow
Inspect the target compiler release/toolchain, framework/mapper versions and configuration,
construction paths and published failure contracts before editing validation. No single
authoring baseline is declared; references use Java SE 25, records require Java 16+ and
String.strip Java 11+. Use ordinary validated classes or the existing compatible policy on
older targets; do not upgrade Java/frameworks or enable preview. Missing mapper/caller evidence
means check removal remains conditional, not proven safe.
- Identify the trust boundaries — where data arrives from code you do not control:
deserialised requests, message payloads, file and database reads, configuration,
and every public entry point of a published library. When unsure whether a seam is a
boundary, read references/trust-boundaries.md.
- Bound before expensive work. Limit bytes, nesting, collection counts and decompressed
expansion; decode strictly; then apply only contract-defined canonicalization and validate
semantics. Preserve raw input separately only when audit/legal needs justify its risk.
- Make the validated state a type. Parse raw input into a record whose compact
constructor enforces the checks. A non-null
CustomerId can carry its component's
format invariant across trusted calls. The variable holding that record can still be
null; mutable components and unverified construction paths need separate evidence.
- Delete only proven-redundant checks. Keep constructor invariants, authorization,
concurrency/transaction rechecks and checks protecting a different state transition.
- Use assertions diagnostically, never as required enforcement. If disabling a check could
permit corruption, disclosure or an invalid side effect, use an explicit runtime check.
Rules
- Preconditions identify the field/expectation with a stable error code. Include actual values only
when they are bounded and non-sensitive; otherwise redact/hash and retain a correlation id.
- Fail fast before irreversible effects for one invalid operation. Batch/stream boundaries may
isolate bad items and return an aggregate report, but must not acknowledge invalid work as
successful or continue with corrupted shared state.
- Do not silently change meaning. Defaults, clamping and migration coercions are acceptable only as
an explicit, versioned compatibility policy with telemetry and a removal/ownership decision.
Representation normalization is likewise contract-specific: case, whitespace and Unicode changes
can alter identifiers, signatures or user-visible text.
assert is disabled by default (enabled with -ea) and must have no required side effects. Use it
for diagnostic internal claims whose removal does not change correctness. Public/trust-boundary
preconditions and corruption-prevention invariants require ordinary control flow/exceptions.
- Remove repeated component checks only after establishing a non-null validated object,
invariant-preserving accessors and safe ownership. Constructor validation does not make
the record reference non-null or mutable component contents permanently valid.
- No catch-all "just in case" wrappers around interior calls. Exception handling
strategy — what to catch where — belongs to java-exception-design.
- A published library's public methods are compatibility/trust boundaries even when current callers
are internal. Enforce the documented contract; do not mechanically check every parameter when a
natural operation already provides the same stable failure and the performance/API policy says
so.
- Defend availability as well as value correctness: cap input/body/collection sizes, nesting,
decompression ratios, regex/parser work, numeric ranges and per-request concurrency before
allocating proportional state. Apply deadlines/cancellation at blocking boundaries. A syntactically
valid payload can still be a resource-exhaustion attack.
- Validation is not authorization and escaping is sink-specific. Revalidate tenant/resource access
at the operation, and parameterize/escape where data enters SQL, HTML, shells, paths or logs;
java-application-security-basics and java-strings-and-text own those controls.
References
Deliver each added/removed check with its owning boundary or state transition, failure
contract, and tests executed. Exercise hostile input, direct construction and bypass paths;
verify invalid input causes no protected effect. Distinguish proposed framework/error-mapping
checks from executed results, and keep regression coverage for every supported entry point.
- Trust boundaries — how to find the boundaries in a
real codebase, heuristics for ambiguous seams, and the checks that look redundant but
are load-bearing. Read before deleting any existing check.
- Worked example: hardening one boundary — before →
after on a refund endpoint, including the interior checks the change deletes. Read
when applying the workflow to real code.
1---2name: java-defensive-programming3description: Where to defend in Java and where defence becomes noise: trust boundaries as the organising idea, preconditions with Objects.requireNonNull and explicit range and state checks, fail-fast over limping on, input normalisation at the edge, and assert for internal invariants only. Use when adding or reviewing validation, when the same invariant is re-checked on every layer, when code silently "corrects" bad input or wraps everything in catch-alls, or when hardening a public API. Does not cover contract semantics and Javadoc documentation (java-design-by-contract), nullability contracts and annotations (java-null-safety), defensive copy mechanics (java-immutability), or the design of the exceptions thrown (java-exception-design).4---56# Java Defensive Programming78## Purpose910Concentrate each defence at the boundary or state transition that owns its invariant, then remove11only checks proven redundant. The two failure12modes this skill prevents are opposites: the unguarded boundary that lets bad data deep13into the system before anything fails, and the codebase where every private method14re-checks every argument — noise that buries the checks that matter and asserts that15nobody knows where validation actually happened.1617## Workflow1819Inspect the target compiler release/toolchain, framework/mapper versions and configuration,20construction paths and published failure contracts before editing validation. No single21authoring baseline is declared; references use Java SE 25, records require Java 16+ and22`String.strip` Java 11+. Use ordinary validated classes or the existing compatible policy on23older targets; do not upgrade Java/frameworks or enable preview. Missing mapper/caller evidence24means check removal remains conditional, not proven safe.25261. **Identify the trust boundaries** — where data arrives from code you do not control:27 deserialised requests, message payloads, file and database reads, configuration,28 and every public entry point of a published library. When unsure whether a seam is a29 boundary, read [references/trust-boundaries.md](references/trust-boundaries.md).302. **Bound before expensive work.** Limit bytes, nesting, collection counts and decompressed31 expansion; decode strictly; then apply only contract-defined canonicalization and validate32 semantics. Preserve raw input separately only when audit/legal needs justify its risk.333. **Make the validated state a type.** Parse raw input into a record whose compact34 constructor enforces the checks. A non-null `CustomerId` can carry its component's35 format invariant across trusted calls. The variable holding that record can still be36 null; mutable components and unverified construction paths need separate evidence.374. **Delete only proven-redundant checks.** Keep constructor invariants, authorization,38 concurrency/transaction rechecks and checks protecting a different state transition.395. **Use assertions diagnostically, never as required enforcement.** If disabling a check could40 permit corruption, disclosure or an invalid side effect, use an explicit runtime check.4142## Rules4344- Preconditions identify the field/expectation with a stable error code. Include actual values only45 when they are bounded and non-sensitive; otherwise redact/hash and retain a correlation id.46- Fail fast before irreversible effects for one invalid operation. Batch/stream boundaries may47 isolate bad items and return an aggregate report, but must not acknowledge invalid work as48 successful or continue with corrupted shared state.49- Do not silently change meaning. Defaults, clamping and migration coercions are acceptable only as50 an explicit, versioned compatibility policy with telemetry and a removal/ownership decision.51 Representation normalization is likewise contract-specific: case, whitespace and Unicode changes52 can alter identifiers, signatures or user-visible text.53- `assert` is disabled by default (enabled with `-ea`) and must have no required side effects. Use it54 for diagnostic internal claims whose removal does not change correctness. Public/trust-boundary55 preconditions and corruption-prevention invariants require ordinary control flow/exceptions.56- Remove repeated component checks only after establishing a non-null validated object,57 invariant-preserving accessors and safe ownership. Constructor validation does not make58 the record reference non-null or mutable component contents permanently valid.59- No catch-all "just in case" wrappers around interior calls. Exception handling60 strategy — what to catch where — belongs to java-exception-design.61- A published library's public methods are compatibility/trust boundaries even when current callers62 are internal. Enforce the documented contract; do not mechanically check every parameter when a63 natural operation already provides the same stable failure and the performance/API policy says64 so.65- Defend availability as well as value correctness: cap input/body/collection sizes, nesting,66 decompression ratios, regex/parser work, numeric ranges and per-request concurrency before67 allocating proportional state. Apply deadlines/cancellation at blocking boundaries. A syntactically68 valid payload can still be a resource-exhaustion attack.69- Validation is not authorization and escaping is sink-specific. Revalidate tenant/resource access70 at the operation, and parameterize/escape where data enters SQL, HTML, shells, paths or logs;71 java-application-security-basics and java-strings-and-text own those controls.7273## References7475Deliver each added/removed check with its owning boundary or state transition, failure76contract, and tests executed. Exercise hostile input, direct construction and bypass paths;77verify invalid input causes no protected effect. Distinguish proposed framework/error-mapping78checks from executed results, and keep regression coverage for every supported entry point.7980- [Trust boundaries](references/trust-boundaries.md) — how to find the boundaries in a81 real codebase, heuristics for ambiguous seams, and the checks that look redundant but82 are load-bearing. Read before deleting any existing check.83- [Worked example: hardening one boundary](references/hardening-example.md) — before →84 after on a refund endpoint, including the interior checks the change deletes. Read85 when applying the workflow to real code.