Exhaustiveness is a change alarm.
Java sealed classes name the permitted subtypes of a class or interface. Pattern matching for switch became final in Java 21. The compiler can check that a pattern switch covers the selector type.[1]
That check is useful because it turns a missing case into a build failure. A catch-all branch weakens the alarm. The code still compiles when the type grows, but the new value falls into a generic path that may hide the change from review.
A closed type does not remove change. It chooses where change should make noise.
Separate compilation opens a side door.
Neil Madden demonstrates the boundary with a sealed Java interface. A consumer compiles an exhaustive switch for TypeA and TypeB. The producer then adds TypeC. If the consumer is not recompiled, sending the new value through the old switch can throw MatchException at runtime.[2]
The original compile was correct for the type it saw. The stale binary never saw the new permitted subtype. Exhaustiveness therefore depends on type closure and build closure. Your release route must bring every affected consumer back through compilation.
A rebuild is necessary, not sufficient.
Recompiling the consumer exposes the missing branch. Adding the branch restores compilation. Neither step proves that the branch does the right work. Send a real TypeC value through the updated path and inspect the result.
Keep these records separate:
- The producer revision that adds the subtype.
- The consumer revision compiled against that producer.
- The compiler output before the missing branch is fixed.
- The runtime result for the new value after the branch is added.
Other languages use different hardware.
TypeScript can narrow a discriminated union to never after every member is handled. The handbook shows two common checks: assign the remaining value to never, or pass it to an assertNever function.[3] The check still depends on updated type declarations and a compiler run.
Rust requires match expressions to cover every possible case of the matched value. The Rust Book shows the compiler rejecting a missing Option<T> case and explains that a wildcard pattern can satisfy the requirement.[4] That wildcard has the same review cost as a Java default: future cases can pass without naming their behavior.
Do not flatten these languages into one rule. Java class evolution, TypeScript's erased runtime, and Rust enum compilation have different package and runtime boundaries. The reusable habit is narrower. Keep the finite type, the matching code, the build, and the new-case test in one change packet.
Ship the alarm with the API.
PRODUCER_REVISION=[REQUIRED]
CONSUMER_REVISION=[REQUIRED]
DECLARED_CASES=[A, B, C]
CATCH_ALL_BRANCH=[ABSENT OR JUSTIFIED]
REBUILD_COMMAND=[REQUIRED]
MISSING_CASE_DIAGNOSTIC=[SAVED]
NEW_CASE_FIXTURE=[REQUIRED]
NEW_CASE_RESULT=[REQUIRED]
RELEASE_DECISION=[REQUIRED]
The walkthrough below makes the Java sequence visible. It teaches the expected checkpoints. It does not compile code or prove a release.