Final StretchOrdered learning track

Type System Failure Modes & Production Postmortems

Learn Java Data Types, Type Semantics, Object Model & Data Representation - Part 033

Catalog of production failure modes caused by weak type modeling, conversion mistakes, temporal ambiguity, equality bugs, numeric corruption, nullability, serialization loss, and boundary leakage.

15 min read2946 words
PrevNext
Lesson 3334 lesson track2934 Final Stretch
#java#data-types#type-system#production-failures+3 more

Part 033 — Type System Failure Modes & Production Postmortems

Target skill: mampu membaca incident production bukan hanya sebagai “bug implementasi”, tetapi sebagai kegagalan representasi data, invariant, boundary, dan type contract.

Part ini adalah bagian penutup sebelum capstone. Setelah kita membahas primitive, reference, class, interface, record, enum, array, boxing, conversion, String, Unicode, BigDecimal, time, byte, identifier, value object, serialization boundary, dan memory/performance, sekarang kita gunakan semuanya sebagai alat diagnosis.

Seorang engineer senior tidak hanya bertanya:

“Baris kode mana yang salah?”

Tetapi:

“Mengapa tipe yang kita pilih mengizinkan state salah itu masuk, menyebar, dan terlihat valid sampai production?”

Itulah inti part ini.


1. Kaufman Angle: Failure Mode as Feedback Loop

Dalam kerangka Josh Kaufman, skill mastery dipercepat ketika kita:

  1. Memecah skill menjadi subskill kecil.
  2. Belajar cukup untuk bisa self-correct.
  3. Menghapus hambatan praktik.
  4. Melatih subskill paling bernilai secara sengaja.

Untuk type mastery, feedback loop terbaik adalah postmortem.

Tujuan part ini bukan menghafal daftar bug. Tujuannya membangun refleks:

  • ketika melihat bug angka, pikirkan range, precision, scale, rounding, promotion, overflow;
  • ketika melihat bug tanggal, pikirkan instant, local time, zone, business calendar, valid time, transaction time;
  • ketika melihat bug data API, pikirkan type loss, null-vs-absent, enum evolution, JSON number precision;
  • ketika melihat bug collection/cache, pikirkan equality, hashCode, mutability, identity;
  • ketika melihat bug authorization/case workflow, pikirkan state model, closed domain, transition invariant.

2. The Production Failure Taxonomy

Sebagian besar incident tipe jatuh ke salah satu kategori berikut.

CategoryRoot CauseTypical SymptomStronger Type Move
Numeric overflowRange tidak divalidasitotal negatif, counter wrapMath.addExact, range value object
Floating approximationdouble dipakai untuk exact domaininvoice/money mismatchBigDecimal + rounding policy
Decimal scale mismatchBigDecimal.equals/scale salahduplicate key, failed lookupcanonical scale wrapper
Null ambiguitynull berarti banyak halNPE, wrong authorization, missing dataexplicit absence type/result
Equality/hash bugmutable key atau equality salahcache miss, duplicate set entryimmutable key, stable equality
Enum evolutionexternal value disamakan dengan enum name/ordinalAPI break, DB corruptionstable code + unknown handling
Text encodingbytes dianggap text tanpa charsetcorrupt name/address/searchexplicit charset boundary
Unicode normalizationtext visual sama tapi binary bedaduplicate account, failed matchingnormalization policy
Timezone/DSTlocal time dipakai sebagai instantmissed deadline, double executionexplicit temporal type
ID/key confusionpublic/internal/natural key tercampurdata leak, wrong tenant accesstyped IDs + scoped key
Boxing/unboxingwrapper dianggap primitiveNPE, identity compare bugprimitive default or explicit optional
Array/buffer aliasingmutable data keluar boundarysignature mismatch, audit driftdefensive copy/snapshot
Serialization lossJava type semantics hilang di JSON/DBprecision loss, null issue, enum breakschema contract + adapters
Flag explosionboolean flags menggantikan stateimpossible combinationenum/state machine/sealed type
Mutable record componentrecord dianggap deep immutablecache/audit value changescopy in constructor/accessor

Pola umumnya sama:

Bug paling mahal jarang terjadi saat data pertama kali salah. Biasanya bug menjadi mahal karena data salah tetap terlihat type-correct.


Gunakan lensa berikut setiap kali incident berkaitan dengan data.

3.1 Symptom Layer

Tanyakan:

  • Nilai apa yang salah?
  • Di mana pertama kali nilai salah terlihat?
  • Apakah salahnya range, precision, identity, absence, time, ordering, encoding, atau state?
  • Apakah salahnya deterministic atau intermittent?

Contoh:

Symptom:
A penalty deadline was computed one day late for cases in a specific timezone.

Wrong value:
2026-03-30T00:00 local was treated as if it were UTC.

Data category:
Temporal semantics: LocalDateTime used as Instant.

3.2 Invariant Layer

Tanyakan:

  • Invariant apa yang seharusnya tidak pernah dilanggar?
  • Apakah invariant itu ditulis di type constructor, service method, database constraint, test, atau hanya di komentar?
  • Apakah semua entry point melewati invariant yang sama?

Contoh invariant:

record PenaltyWindow(LocalDate startDate, LocalDate endDate) {
    PenaltyWindow {
        Objects.requireNonNull(startDate);
        Objects.requireNonNull(endDate);
        if (endDate.isBefore(startDate)) {
            throw new IllegalArgumentException("endDate must not be before startDate");
        }
    }
}

3.3 Boundary Layer

Tanyakan:

  • Apakah semantics berubah saat melewati JSON, DB, queue, file, cache, log, atau UI?
  • Apakah timezone, scale, currency, charset, enum code, tenant scope, atau unit hilang?
  • Apakah downstream punya asumsi yang sama?

3.4 Recovery Layer

Tanyakan:

  • Apakah data yang salah sudah persisted?
  • Apakah perlu migration/backfill?
  • Apakah event lama perlu replay?
  • Apakah ada audit/legal impact?
  • Apakah type fix backward-compatible?

4. Failure Mode 1 — Integral Overflow and Truncation

Problem

Integral types di Java punya fixed range. int overflow tidak otomatis melempar exception.

int max = Integer.MAX_VALUE;
int broken = max + 1; // wraps to Integer.MIN_VALUE

Bug ini sering muncul di:

  • counter;
  • pagination offset;
  • duration dalam milliseconds;
  • financial quantity minor unit;
  • batch size multiplication;
  • array/buffer length calculation;
  • hash or scoring formula.

Bad Production Shape

int totalBytes = recordCount * averageRecordSize;
byte[] payload = new byte[totalBytes];

Jika multiplication overflow menjadi negatif atau terlalu kecil, hasilnya bisa:

  • NegativeArraySizeException;
  • allocation size salah;
  • buffer under-allocation;
  • truncated payload;
  • security issue bila length validation salah.

Stronger Fix

record PayloadSize(int bytes) {
    private static final int MAX_PAYLOAD_BYTES = 10 * 1024 * 1024;

    PayloadSize {
        if (bytes < 0 || bytes > MAX_PAYLOAD_BYTES) {
            throw new IllegalArgumentException("payload size out of range: " + bytes);
        }
    }

    static PayloadSize fromRecordEstimate(int recordCount, int averageRecordSize) {
        int bytes = Math.multiplyExact(recordCount, averageRecordSize);
        return new PayloadSize(bytes);
    }
}

Review Questions

  • Apakah arithmetic bisa overflow sebelum di-cast ke long?
  • Apakah multiplication terjadi pada int lalu baru assignment ke long?
  • Apakah range domain lebih kecil dari range tipe Java?
  • Apakah invalid value harus gagal cepat?

5. Failure Mode 2 — Floating-Point Used for Exact Domain

Problem

float dan double cocok untuk approximate arithmetic, bukan exact business arithmetic.

double a = 0.1;
double b = 0.2;
System.out.println(a + b); // not exactly 0.3

Bad Production Shape

record Fine(double amount) {}

Masalah:

  • tidak ada currency;
  • tidak ada scale;
  • tidak ada rounding policy;
  • equality unstable;
  • serialization bisa memberi representasi mengejutkan;
  • audit tidak defensible untuk uang.

Stronger Fix

record Money(BigDecimal amount, Currency currency) {
    Money {
        Objects.requireNonNull(amount);
        Objects.requireNonNull(currency);
        amount = amount.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_UP);
        if (amount.signum() < 0) {
            throw new IllegalArgumentException("money must not be negative");
        }
    }

    Money plus(Money other) {
        requireSameCurrency(other);
        return new Money(amount.add(other.amount), currency);
    }

    private void requireSameCurrency(Money other) {
        if (!currency.equals(other.currency)) {
            throw new IllegalArgumentException("currency mismatch");
        }
    }
}

Review Questions

  • Apakah domain butuh exact arithmetic?
  • Apakah ada currency/unit?
  • Apakah rounding policy eksplisit?
  • Apakah comparison memakai policy domain, bukan raw equals sembarangan?

6. Failure Mode 3 — BigDecimal Scale and Equality Trap

Problem

BigDecimal.compareTo dan BigDecimal.equals punya semantics berbeda.

new BigDecimal("1.0").compareTo(new BigDecimal("1.00")) == 0; // true
new BigDecimal("1.0").equals(new BigDecimal("1.00"));        // false

Jika BigDecimal dipakai sebagai key tanpa canonicalization, lookup bisa gagal.

Bad Production Shape

Map<BigDecimal, FeeRule> rulesByRate = new HashMap<>();
rulesByRate.put(new BigDecimal("0.10"), rule);

FeeRule found = rulesByRate.get(new BigDecimal("0.100")); // null

Stronger Fix

record Rate(BigDecimal value) {
    private static final int SCALE = 6;

    Rate {
        Objects.requireNonNull(value);
        value = value.setScale(SCALE, RoundingMode.UNNECESSARY);
        if (value.signum() < 0) {
            throw new IllegalArgumentException("rate must not be negative");
        }
    }
}

Review Questions

  • Apakah scale bagian dari identity domain?
  • Apakah semua input dinormalisasi?
  • Apakah map/set key memakai canonical representation?
  • Apakah DB column scale cocok dengan Java scale?

7. Failure Mode 4 — Null Means Too Many Things

Problem

null sering dipakai untuk banyak arti sekaligus:

  • belum dimuat;
  • tidak ditemukan;
  • tidak berlaku;
  • disembunyikan karena authorization;
  • error saat mengambil data;
  • field memang kosong;
  • input tidak dikirim.

Ini berbahaya karena semua arti berbeda itu memakai value yang sama.

Bad Production Shape

String assignedOfficerId = caseRecord.assignedOfficerId();

if (assignedOfficerId == null) {
    autoAssign(caseRecord);
}

Apakah null berarti belum assigned? Atau user tidak berhak melihat officer? Atau migration lama belum mengisi? Atau data corrupt?

Stronger Fix

sealed interface AssignmentStatus permits Unassigned, Assigned, RestrictedAssignmentView {}

record Unassigned() implements AssignmentStatus {}
record Assigned(OfficerId officerId) implements AssignmentStatus {}
record RestrictedAssignmentView() implements AssignmentStatus {}

Sekarang state berbeda menjadi tipe berbeda.

Review Questions

  • Apakah null punya lebih dari satu arti?
  • Apakah parameter nullable didokumentasikan dan diuji?
  • Apakah Optional hanya dipakai pada boundary return, bukan field/domain state sembarangan?
  • Apakah authorization tidak dicampur dengan absence?

8. Failure Mode 5 — Mutable Equality Key

Problem

Hash-based collections mengandalkan hash code stabil selama object berada di collection.

Bad Production Shape

class CaseKey {
    private String tenantId;
    private String caseNumber;

    // equals/hashCode use tenantId and caseNumber

    void setCaseNumber(String caseNumber) {
        this.caseNumber = caseNumber;
    }
}

Jika caseNumber berubah setelah object masuk HashMap, entry bisa tidak ditemukan.

Stronger Fix

record CaseKey(TenantId tenantId, CaseNumber caseNumber) {
    CaseKey {
        Objects.requireNonNull(tenantId);
        Objects.requireNonNull(caseNumber);
    }
}

Review Questions

  • Apakah field yang dipakai equality immutable?
  • Apakah object dipakai sebagai key di map/set/cache?
  • Apakah equality berdasarkan identity, natural key, atau surrogate key?
  • Apakah object entity berubah lifecycle-nya setelah persisted?

9. Failure Mode 6 — Enum Evolution Breaks External Contracts

Problem

Enum bagus untuk closed domain. Tetapi enum buruk jika external contract memakai ordinal() atau name() tanpa strategi evolusi.

Bad Production Shape

// Stored in DB
status.ordinal();

Jika enum order berubah, data lama berubah arti.

Better Shape

enum CaseStatus {
    DRAFT("DRAFT"),
    SUBMITTED("SUBMITTED"),
    UNDER_REVIEW("UNDER_REVIEW"),
    CLOSED("CLOSED");

    private final String code;

    CaseStatus(String code) {
        this.code = code;
    }

    String code() {
        return code;
    }

    static Optional<CaseStatus> fromCode(String code) {
        return Arrays.stream(values())
            .filter(status -> status.code.equals(code))
            .findFirst();
    }
}

Enterprise Concern

Jika sistem menerima enum value dari external system, pertimbangkan model:

sealed interface ExternalCaseStatus permits KnownExternalCaseStatus, UnknownExternalCaseStatus {}
record KnownExternalCaseStatus(CaseStatus status) implements ExternalCaseStatus {}
record UnknownExternalCaseStatus(String rawCode) implements ExternalCaseStatus {}

Ini membuat integrasi lebih tahan terhadap enum value baru.


10. Failure Mode 7 — Text Encoding and Unicode Normalization

Problem

Text bukan hanya String. Text punya:

  • charset;
  • encoding;
  • normalization;
  • locale;
  • collation;
  • case mapping;
  • grapheme cluster;
  • byte boundary.

Bad Production Shape

String body = new String(bytes); // platform default charset

Kode ini bergantung pada default charset runtime. Untuk boundary enterprise, ini terlalu implisit.

Better Shape

String body = new String(bytes, StandardCharsets.UTF_8);
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);

Normalization Example

Dua string bisa terlihat sama tetapi punya byte representation berbeda.

String normalized = Normalizer.normalize(input, Normalizer.Form.NFC);

Review Questions

  • Apakah setiap byte-to-text conversion menyebut charset?
  • Apakah user-facing identifier butuh normalization?
  • Apakah comparison harus case-sensitive, locale-sensitive, atau binary exact?
  • Apakah hashing/signature dilakukan sebelum atau sesudah normalization?

11. Failure Mode 8 — Temporal Ambiguity

Problem

Tanggal/waktu production bug biasanya bukan karena Java API kurang. Biasanya karena model domain salah.

Contoh ambiguity:

LocalDateTime deadline = LocalDateTime.parse("2026-03-29T02:30:00");

Apakah ini:

  • local wall-clock time di zona tertentu?
  • instant global?
  • business deadline date?
  • timestamp event diterima?
  • valid-time domain?
  • transaction-time record?

Stronger Type Split

record SubmittedAt(Instant value) {}
record EffectiveDate(LocalDate value) {}
record BusinessDeadline(ZonedDateTime value) {}
record RecordedAt(Instant value) {}

Failure Modes

FailureCauseFix
Deadline missedLocalDateTime tanpa zoneuse ZonedDateTime or domain deadline type
Report shiftedDB timezone conversionexplicit storage contract
Duplicate job runDST overlapidempotent schedule key
Invalid local timeDST gapresolve policy explicitly
Wrong audit orderinglocal timestamp compared across zonesuse Instant for timeline ordering

Review Questions

  • Apakah waktu ini timeline atau calendar?
  • Apakah timezone bagian dari data?
  • Apakah business day sama dengan 24 hours?
  • Apakah valid time dan transaction time terpisah?

12. Failure Mode 9 — ID and Key Confusion

Problem

String id adalah smell jika domain punya banyak jenis ID.

void assign(String caseId, String officerId, String tenantId) {}

Bug yang mungkin:

assign(officerId, caseId, tenantId); // compiles

Stronger Shape

record TenantId(UUID value) {}
record CaseId(UUID value) {}
record OfficerId(UUID value) {}

void assign(CaseId caseId, OfficerId officerId, TenantId tenantId) {}

Enterprise Failure Modes

  • internal DB ID bocor ke public API;
  • cross-tenant access karena key tidak scoped;
  • idempotency key dipakai ulang di context salah;
  • correlation ID disamakan dengan business ID;
  • natural key berubah tetapi dipakai sebagai immutable reference;
  • public ID predictable sehingga memudahkan enumeration attack.

Review Questions

  • Apakah ID punya scope tenant?
  • Apakah ID public berbeda dari primary key internal?
  • Apakah ID sortable, random, deterministic, atau semantic?
  • Apakah idempotency key mengandung operation scope?

13. Failure Mode 10 — Byte Array and Buffer Aliasing

Problem

byte[] mutable. Jika byte array keluar boundary tanpa copy, pemilik luar bisa mengubah data internal.

Bad Shape

record Signature(byte[] bytes) {}

Record ini terlihat immutable, tetapi tidak deep immutable.

byte[] raw = loadSignature();
Signature signature = new Signature(raw);
raw[0] = 42; // signature berubah

Better Shape

record Signature(byte[] bytes) {
    Signature {
        bytes = bytes.clone();
    }

    @Override
    public byte[] bytes() {
        return bytes.clone();
    }
}

Review Questions

  • Apakah array/buffer keluar dari aggregate?
  • Apakah object mengklaim immutable tetapi punya mutable component?
  • Apakah hash/signature dihitung dari snapshot?
  • Apakah ByteBuffer.slice()/duplicate() masih berbagi backing storage?

14. Failure Mode 11 — Boxing and Wrapper Identity

Problem

Wrapper object bukan primitive.

Integer a = 1000;
Integer b = 1000;
System.out.println(a == b); // false in common implementations

Lebih buruk lagi:

Boolean enabled = null;
if (enabled) { // NullPointerException from unboxing
    run();
}

Better Shape

enum FeatureDecision {
    ENABLED,
    DISABLED,
    NOT_CONFIGURED
}

Review Questions

  • Apakah wrapper nullable dengan sengaja?
  • Apakah == dipakai pada wrapper?
  • Apakah collection memaksa boxing di hot path?
  • Apakah overload method menerima primitive dan wrapper sekaligus?

15. Failure Mode 12 — Serialization Boundary Type Loss

Problem

Saat Java object menjadi JSON, DB row, Kafka message, log, atau CSV, banyak semantics hilang.

Java side:

record Payment(Money amount, Instant submittedAt, CaseStatus status) {}

JSON side:

{
  "amount": "100.00",
  "currency": "IDR",
  "submittedAt": "2026-06-30T10:15:30Z",
  "status": "SUBMITTED"
}

Yang harus dijaga:

  • amount numeric atau string?
  • scale canonical?
  • timezone format?
  • enum unknown handling?
  • null vs absent?
  • backward compatibility?
  • schema version?

Boundary Contract Checklist

TypeBoundary RiskContract Rule
BigDecimalprecision lossencode as string or exact decimal policy
Instanttimezone lostISO-8601 UTC with Z or explicit zone contract
LocalDateinterpreted as instantnever attach timezone implicitly
enumnew value breaks consumerstable code + unknown strategy
UUIDstring format variancecanonical lowercase string
bytesbinary not JSON-safebase64 with size limit
optionalnull vs absentdefine PATCH/create semantics
recordconstructor invariants bypassed by frameworkvalidate after deserialization

16. Failure Mode 13 — Boolean Flag Explosion

Problem

Boolean flags terlihat sederhana sampai kombinasi state menjadi tidak masuk akal.

record CaseView(
    boolean submitted,
    boolean approved,
    boolean rejected,
    boolean closed,
    boolean reopened
) {}

Kombinasi invalid:

approved=true, rejected=true
closed=false, reopened=true
submitted=false, approved=true

Better Shape

enum CaseLifecycleStatus {
    DRAFT,
    SUBMITTED,
    UNDER_REVIEW,
    APPROVED,
    REJECTED,
    CLOSED,
    REOPENED
}

Untuk transition rule:

record CaseLifecycle(CaseLifecycleStatus status) {
    CaseLifecycle transitionTo(CaseLifecycleStatus next) {
        if (!allowed(status, next)) {
            throw new IllegalStateException("invalid transition: " + status + " -> " + next);
        }
        return new CaseLifecycle(next);
    }

    private static boolean allowed(CaseLifecycleStatus from, CaseLifecycleStatus to) {
        return switch (from) {
            case DRAFT -> to == CaseLifecycleStatus.SUBMITTED;
            case SUBMITTED -> to == CaseLifecycleStatus.UNDER_REVIEW;
            case UNDER_REVIEW -> to == CaseLifecycleStatus.APPROVED || to == CaseLifecycleStatus.REJECTED;
            case APPROVED, REJECTED -> to == CaseLifecycleStatus.CLOSED;
            case CLOSED -> to == CaseLifecycleStatus.REOPENED;
            case REOPENED -> to == CaseLifecycleStatus.UNDER_REVIEW;
        };
    }
}

Review Questions

  • Apakah lebih dari satu boolean menggambarkan satu state machine?
  • Apakah ada impossible state?
  • Apakah transition rule tersebar di UI/service/DB?
  • Apakah audit mencatat transition, bukan hanya current flag?

17. Failure Mode 14 — Record Misused as Deeply Immutable Object

Problem

Record memberi final fields dan generated equality, tetapi tidak otomatis membuat component object immutable.

Bad Shape

record CaseSnapshot(List<String> tags) {}

Jika list mutable disimpan langsung, snapshot bisa berubah.

Better Shape

record CaseSnapshot(List<String> tags) {
    CaseSnapshot {
        tags = List.copyOf(tags);
    }
}

Review Questions

  • Apakah record component mutable?
  • Apakah constructor membuat snapshot?
  • Apakah accessor membocorkan mutable internal state?
  • Apakah equality aman terhadap mutation?

18. Incident Template: Type-Centric Postmortem

Gunakan template ini untuk incident yang berkaitan dengan data correctness.

# Type-Centric Postmortem

## Incident Summary
- What happened?
- User/business impact?
- First detection time?
- Recovery time?

## Wrong Value
- What value was wrong?
- What was the expected value?
- What type represented it?
- Was the wrong value syntactically valid?

## Invariant Failure
- Which invariant was missing or unenforced?
- Where should the invariant live?
- Which entry point bypassed it?

## Boundary Failure
- Did the value cross JSON, DB, queue, cache, file, log, UI, or external API?
- What semantics were lost?
- Was conversion implicit?

## Propagation
- Which systems trusted the wrong value?
- Was the wrong value persisted?
- Were events emitted?
- Are reports/audits affected?

## Type Fix
- Stronger type introduced?
- Constructor/factory validation?
- Serialization adapter changed?
- Migration/backfill needed?

## Tests Added
- Unit tests?
- Property tests?
- Contract tests?
- Boundary tests?
- Regression data fixture?

## Compatibility
- API compatibility?
- DB migration?
- Event replay?
- Consumer impact?

## Prevention
- Static analysis rule?
- Code review checklist?
- Shared type library?
- Documentation update?

19. Type Failure Review Matrix

Review AreaGreenYellowRed
Numericrange and rounding explicitprimitive used with commentssilent overflow/rounding
Moneyamount + currency + policyBigDecimal onlydouble money
Nullabilityexplicit absence modelnullable documentednull means many things
Equalityimmutable stable keyequality uses some mutable datamutable hash key
Enumstable code + unknown policyname() externalizedordinal() persisted
Textcharset explicitcharset implied in one boundaryplatform default used
Timesemantic type explicitLocalDateTime documentedlocal time used as instant
IDtyped scoped IDraw String but named carefullymixed ID parameters
Bytesdefensive copiesconvention-based ownershipmutable array leaked
Serializationcontract testsmapper config onlyimplicit framework defaults
Statestate machine/enumseveral flags with checksimpossible states possible
Performancemeasured hot pathguessed costwrapper/object graph in hot loop without measurement

20. Production Postmortem Mini-Cases

Case A — Duplicate Penalty Because of DST Overlap

Symptom: scheduled penalty job runs twice for same local time.

Wrong model: LocalDateTime used as unique execution key.

record JobRunKey(LocalDateTime scheduledAt) {}

Failure: during DST overlap, local time occurs twice.

Fix: use Instant plus business schedule identity.

record JobRunKey(String scheduleName, Instant executionInstant) {}

Also add idempotency constraint in persistence.


Case B — Cache Miss After Case Number Mutation

Symptom: case exists in cache but lookup returns null.

Wrong model: mutable class used as map key.

Fix: immutable record CaseKey(TenantId tenantId, CaseNumber caseNumber).

Also make case number change create a new key mapping, not mutate existing key.


Case C — External Status Breaks Consumer

Symptom: integration starts failing after upstream adds status SUSPENDED.

Wrong model: direct enum deserialization with no unknown handling.

Fix: parse into KnownStatus or UnknownStatus(rawCode) and route unknown values to quarantine/review.


Case D — Fine Amount Off by One Cent

Symptom: total fine differs between invoice service and reporting service.

Wrong model: percentage calculation done with different rounding places.

Fix: centralize Rate, Money, RoundingPolicy, and allocation logic.


Case E — Audit Evidence Hash Changes

Symptom: stored evidence digest no longer matches payload.

Wrong model: byte[] stored in record without copy; buffer later reused.

Fix: immutable EvidenceBytes with defensive copy and digest computed from snapshot.


21. Deliberate Practice

Practice 1 — Classify the Failure

Given this code:

record FineRule(double rate, String currency, String status) {}

Classify at least five failure modes.

Expected findings:

  • double for exact rate;
  • currency as raw string;
  • status as raw string;
  • no range validation;
  • no rounding policy;
  • no enum unknown strategy;
  • no semantic type for rate;
  • possible invalid currency code.

Practice 2 — Refactor to Stronger Types

Refactor:

void submit(String tenantId, String caseId, String officerId, String submittedAt) {}

Better:

void submit(TenantId tenantId, CaseId caseId, OfficerId officerId, SubmittedAt submittedAt) {}

Then define each type with validation.

Practice 3 — Boundary Contract Test

Write tests for JSON payload:

{
  "amount": "100.00",
  "currency": "IDR",
  "submittedAt": "2026-06-30T10:15:30Z",
  "status": "SUBMITTED"
}

Test:

  • amount scale;
  • currency code;
  • UTC instant;
  • unknown enum behavior;
  • null vs absent;
  • extra field tolerance;
  • invalid number rejection.

22. Engineering Handbook Checklist

Before merging a data model change, ask:

  1. What invalid states does this type prevent?
  2. What invalid states can still be represented?
  3. Which fields participate in equality?
  4. Are equality fields immutable?
  5. Are any components mutable?
  6. Are arrays/collections defensively copied?
  7. Does this type cross JSON/DB/event/API boundaries?
  8. Does serialization preserve semantics?
  9. Are numeric range, scale, precision, unit, and rounding explicit?
  10. Are temporal semantics explicit?
  11. Is null used for exactly one meaning?
  12. Are enum values externally stable?
  13. Are ID scopes encoded?
  14. Are text charset/normalization assumptions explicit?
  15. Is there a regression test for the most likely production failure?

23. Summary

Type-related production bugs usually are not caused by Java “not being safe enough”. They are caused by weak modeling choices that allow invalid values to look valid.

The most important engineering shift is this:

Do not use types only to satisfy the compiler. Use types to make illegal states difficult, suspicious states visible, and boundary conversions explicit.

In the next and final part, we will apply the entire series in a capstone: designing a type-safe enterprise data model for a regulatory case lifecycle system.


References

Lesson Recap

You just completed lesson 33 in final stretch. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.