ORM Failure Modes
Learn Java Data Access Pattern In Action - Part 042
ORM failure modes dalam Java/JPA/Hibernate production: cartesian explosion, hidden query, detached entity confusion, cascade disaster, lazy exception, merge overwrite, stale persistence context, bulk bypass, equals/hashCode bug, dan prevention checklist.
Part 042 — ORM Failure Modes
ORM memberi productivity tinggi, tetapi failure mode-nya sering tidak terlihat dari kode.
Satu getter bisa query database.
Satu
mergebisa overwrite data.Satu collection replacement bisa delete rows.
Satu fetch join bisa membuat cartesian explosion.
Satu bulk update bisa membuat persistence context stale.
Satu
@DataLombok bisa trigger lazy loading daritoString.Top engineer bukan hanya tahu "cara pakai Hibernate", tetapi tahu bentuk kegagalan yang mungkin muncul dan cara mencegahnya.
Part ini adalah katalog failure modes ORM/JPA/Hibernate dan playbook pencegahannya.
1. Core Thesis
ORM failure mode muncul ketika object model abstraction menutupi fakta bahwa sistem tetap menjalankan SQL, transaction, lock, constraint, cache, dan network roundtrip.
Prinsip pencegahan:
Make database access visible.
Make transaction boundaries explicit.
Use DTO projections for reads.
Use command DTOs for writes.
Keep entity graph bounded.
Treat cascade/merge/lazy loading as dangerous by default.
Test SQL count and concurrency.
2. Failure Mode: N+1 Query
Symptom
- endpoint lambat;
- SQL count tinggi;
- repeated
select ... where id=?; - DB CPU naik;
- latency linear terhadap page size.
Cause
Lazy association accessed in loop/mapper/serializer.
cases.stream()
.map(c -> c.getAssignedOfficer().getDisplayName())
.toList();
Prevention
- DTO projection;
- fetch join for bounded graph;
- entity graph;
- batch fetch for moderate case;
- query count tests.
3. Failure Mode: Hidden Query in Serializer
Symptom
- SQL executed after service method finished;
- JSON serialization slow;
- lazy initialization exception;
- response includes unexpected nested data.
Cause
Controller returns entity/proxy.
@GetMapping("/{id}")
public CaseFileEntity get(UUID id) {
return repository.findById(id).orElseThrow();
}
Prevention
- response DTO;
- OSIV disabled/tested;
- serialization query test;
- no entity leak.
4. Failure Mode: Cartesian Explosion
Symptom
- one query returns huge row count;
- memory spike;
- duplicate parent rows;
- slow detail endpoint;
- DB sort/hash join heavy.
Cause
Multiple collection fetch joins.
select c
from CaseFileEntity c
left join fetch c.assignments
left join fetch c.documents
left join fetch c.actions
where c.id = :id
Rows multiply:
assignments * documents * actions
Prevention
- separate section queries;
- DTO detail view;
- bounded recent collections;
- read model;
- avoid multiple collection fetch joins.
5. Failure Mode: Pagination With Collection Fetch Join
Symptom
- page contains fewer/more parents than expected;
- Hibernate warning;
- in-memory pagination;
- slow query.
Cause
Fetch join collection with setMaxResults.
Prevention
- two-step fetch: page IDs then fetch graph;
- DTO projection;
- read model;
- avoid collection fetch join in pageable list.
6. Failure Mode: LazyInitializationException
Symptom
could not initialize proxy - no Session
Cause
Lazy association accessed after persistence context closed.
Bad Fix
- enable OSIV blindly;
- change everything to EAGER.
Correct Fix
- load required data in service/query;
- return DTO;
- use explicit fetch plan;
- keep entity inside transaction boundary.
7. Failure Mode: EAGER Graph Explosion
Symptom
- simple
findByIdtriggers many joins/selects; - endpoint over-fetches;
- memory high;
- unexpected SQL.
Cause
Associations set EAGER, especially many-to-one chains/collections.
Prevention
- default associations to LAZY explicitly;
- use fetch plan per use case;
- projection for read path.
8. Failure Mode: Detached Entity Confusion
Symptom
- changes not persisted;
- lazy load fails;
- entity appears valid but stale;
- merge behavior surprises.
Cause
Entity loaded in one transaction, used in another as if managed.
CaseFileEntity entity = service.load(id); // detached after tx
entity.setStatus("APPROVED"); // not tracked
Prevention
- do not pass entity across transaction/API boundaries;
- use command DTO;
- reload managed entity in command transaction;
- apply explicit changes.
9. Failure Mode: Blind Merge Overwrite
Symptom
- user changes lost;
- null fields overwrite existing data;
- security fields changed;
- huge update graph;
- stale form overwrites latest state.
Cause
entityManager.merge(requestBodyEntity);
or merging detached graph.
Prevention
- command DTO with expected version;
- load current managed entity;
- apply intended fields only;
- optimistic lock;
- no entity request body.
10. Failure Mode: Mass Assignment
Symptom
- client updates fields they should not control;
- role/status/tenant changed from request;
- privilege escalation.
Cause
Binding JSON directly to entity and saving/merging.
@PostMapping
public void update(@RequestBody CaseFileEntity entity) {
entityManager.merge(entity);
}
Prevention
- request DTO;
- whitelist mutable fields;
- domain methods;
- authorization;
- no entity as API input.
11. Failure Mode: Cascade Disaster
Symptom
- deleting parent deletes shared reference data;
- huge graph persisted unexpectedly;
- unrelated rows updated/deleted.
Cause
CascadeType.ALL on shared association.
@ManyToOne(cascade = CascadeType.ALL)
private OfficerEntity officer;
Prevention
- cascade only to owned children;
- never cascade remove to shared aggregates/reference data;
- integration tests for delete;
- review cascade on every association.
12. Failure Mode: Orphan Removal Data Loss
Symptom
- child rows disappear after update;
- history/audit lost;
- assignment rows deleted unexpectedly.
Cause
orphanRemoval=true plus collection replacement.
entity.setAssignments(mappedFromRequest);
Prevention
- no public collection setter;
- explicit add/remove/end methods;
- avoid orphan removal for audit/history;
- test child lifecycle.
13. Failure Mode: Collection Clear/Repopulate
Symptom
- delete/insert storm;
- version/audit weirdness;
- FK violations;
- lost child identity.
Cause
entity.getChildren().clear();
request.children().forEach(entity::addChild);
Prevention
- compute delta;
- update existing child by ID;
- add new;
- end/remove explicitly;
- use DAO for large child collection.
14. Failure Mode: equals/hashCode Includes Association
Symptom
- lazy load from equals/toString/hashCode;
- recursion/StackOverflow;
- Set removal fails;
- duplicate child rows;
- high SQL during logging.
Cause
Lombok @Data or manual equals using associations/mutable fields.
Prevention
- no
@Dataon entities; - equality by stable ID convention;
- exclude associations from
toString; - test collection behavior.
15. Failure Mode: toString Triggers Lazy Load
Symptom
- logging entity executes SQL;
- recursive log output;
- performance issue only when debug log enabled.
Cause
toString includes lazy associations.
Prevention
- minimal
toString; - log IDs/status only;
- avoid Lombok generated toString including associations.
16. Failure Mode: Unexpected Update in Read Path
Symptom
- GET request updates
updated_at/version; - read endpoint creates writes;
- missing audit for changed data.
Cause
Managed entity mutated while building read response.
entity.normalize();
return mapper.toDto(entity);
Dirty checking flushes.
Prevention
- DTO projection;
- read-only transaction/hints;
- no mutation in mapper;
- no-update read tests.
17. Failure Mode: Auto Flush Before Query
Symptom
- constraint violation thrown during query;
- update occurs earlier than expected;
- lock acquired before validation completed.
Cause
Mutation before JPQL query with AUTO flush.
entity.setStatus(APPROVED);
validateWithQuery(); // triggers flush
Prevention
- load/validate before mutation;
- avoid unrelated queries after mutation;
- explicit flush only at known point;
- structure command flow.
18. Failure Mode: Invalid Intermediate Entity State
Symptom
- not-null/check constraint fails unexpectedly;
- validation seems to happen later but DB sees invalid state.
Cause
Entity set to temporary invalid state before query/flush.
entity.setStatus(null);
querySomething(); // flush
entity.setStatus(APPROVED);
Prevention
- domain methods perform atomic valid transitions;
- no invalid intermediate state on managed entity;
- use local variables before assigning.
19. Failure Mode: Bulk Update Bypasses Persistence Context
Symptom
- entity still shows old data after bulk update;
- later flush overwrites bulk update;
- version not incremented;
- callbacks not called.
Cause
JPQL/native bulk update bypasses managed entities.
Prevention
- bulk update in separate transaction/context;
flush()before bulk if needed;clear()after bulk;- update version explicitly;
- avoid mixing bulk and managed entity state.
20. Failure Mode: Native SQL Stale Context
Symptom
- direct SQL update done, entity still stale;
- command makes decision on old managed state.
Cause
First-level cache returns existing managed entity.
Prevention
- avoid mixing;
- flush/clear/refresh;
- separate transaction;
- use one access style per use case.
21. Failure Mode: Missing Parent Version Increment
Symptom
- child changes but aggregate version unchanged;
- events carry old version;
- read model ordering wrong;
- stale edit not detected.
Cause
Child insert/update does not dirty parent entity.
Prevention
OPTIMISTIC_FORCE_INCREMENT;- update parent
updatedAt; - explicit version strategy;
- test child mutation increments aggregate version if required.
22. Failure Mode: Optimistic Lock Not Used
Symptom
- lost update;
- last writer wins silently;
- user overwrites another user's changes.
Cause
Mutable entity without @Version or expected version command.
Prevention
@Versionon mutable aggregate root;- expected version in edit command;
- conflict mapping;
- tests with concurrent transactions.
23. Failure Mode: Retrying Optimistic Conflict Blindly
Symptom
- command applies to state user never saw;
- approval/rejection against changed case;
- business correctness bug.
Cause
Retrying user command by reload-and-apply.
Prevention
- return conflict for human/stale edits;
- retry only deterministic safe operations;
- idempotency for duplicate retries.
24. Failure Mode: Pessimistic Lock Held Too Long
Symptom
- lock waits;
- timeout;
- deadlock;
- connection pool saturation;
- p99 spike.
Cause
External call/file/email/report inside transaction holding lock.
Prevention
- short transaction;
- outbox/split phase;
- lock timeout;
- no external I/O inside lock;
- measure transaction duration.
25. Failure Mode: Deadlock Due Lock Order
Symptom
- deadlock exceptions under concurrent load.
Cause
Different code paths lock rows in different order.
Prevention
- global lock ordering;
- smaller transactions;
- indexes;
- retry whole transaction if idempotent;
- deadlock tests for critical flows.
26. Failure Mode: Missing Index Hidden by ORM
Symptom
- JPQL looks simple but SQL scans table;
- latency grows with data;
- DB CPU high.
Cause
No index for generated SQL predicate/order.
Prevention
- capture generated SQL;
- explain plan;
- add composite index matching tenant/filter/order;
- performance smoke tests.
27. Failure Mode: Slow Count Query
Symptom
- pageable endpoint slow even data query fast.
Cause
Page<T> triggers expensive count with joins/distinct.
Prevention
- use
Slice; - optimize count separately;
- approximate/precomputed count;
- count on demand;
- review generated count query.
28. Failure Mode: Unbounded Entity Query
Symptom
- memory spike;
- OOM;
- long transaction;
- slow flush.
Cause
getResultList()
without limit on large table.
Prevention
- page/chunk/keyset;
- projection;
- streaming with resource contract;
- export job.
29. Failure Mode: Large Persistence Context
Symptom
- slow flush;
- high memory;
- GC pressure;
- dirty checking cost.
Cause
Loading/persisting many managed entities in one context.
Prevention
- flush/clear in batch;
- stateless session/JDBC batch;
- smaller transaction chunks;
- DTO projection for reads.
30. Failure Mode: IDENTITY Breaks Batching
Symptom
- insert batching not happening;
- many round trips.
Cause
Database identity generation requires immediate insert to get ID.
Prevention
- sequence with allocation;
- application-generated IDs;
- JDBC batch for bulk;
- measure actual batch behavior.
31. Failure Mode: Dynamic Update Reduces Batching
Symptom
- expected batch update not batching well.
Cause
@DynamicUpdate creates different SQL shapes.
Prevention
- measure trade-off;
- use stable update shape for batch-heavy entity;
- dynamic update only when beneficial.
32. Failure Mode: Query Cache Stale/Churn
Symptom
- memory high;
- low hit rate;
- stale/invalidated queries;
- no performance improvement.
Cause
Query cache used on dynamic dashboard/search.
Prevention
- avoid global query cache;
- use read model/projection/index;
- cache only stable reference queries;
- monitor hit ratio.
33. Failure Mode: Second-Level Cache Stale
Symptom
- entity reads old value after update by native SQL/another service/script.
Cause
Cache not invalidated.
Prevention
- avoid mutable entity cache;
- evict on bulk/native update;
- route writes through cache-aware path;
- TTL/metrics/runbook;
- cache read-only reference data first.
34. Failure Mode: Cache Key Missing Tenant
Symptom
- cross-tenant data leak.
Cause
Application cache key lacks tenant/user scope.
Prevention
- cache key includes tenant and scope;
- tests for tenant isolation;
- do not cache scoped data globally.
35. Failure Mode: Read Model Used for Command Truth
Symptom
- command validates stale projection;
- approval allowed after source changed;
- capacity oversubscribed.
Cause
Command checks dashboard/read model/cache instead of source DB.
Prevention
- command validates source aggregate/table;
- read model only for reads/previews;
- final DB constraint/lock/atomic update.
36. Failure Mode: Entity Callback Side Effects
Symptom
- email/event sent despite rollback;
- hidden audit/outbox behavior;
- unpredictable callback order.
Cause
@PostPersist/@PreUpdate performs business workflow or external I/O.
Prevention
- callbacks only for technical metadata;
- outbox for integration events;
- application service writes audit/outbox explicitly.
37. Failure Mode: Time Source Inconsistency
Symptom
- audit time differs from domain event time;
- test flakiness;
- ordering confusion.
Cause
Mix of DB now(), Instant.now(), callbacks, command time.
Prevention
- explicit clock/time source strategy;
- command carries
requestedAt; - DB time only when intended;
- document timestamp semantics.
38. Failure Mode: Entity as API Contract
Symptom
- schema change breaks API;
- sensitive data leak;
- lazy graph exposed;
- circular JSON.
Cause
JPA entity returned directly.
Prevention
- API response DTO;
- projection/read model;
- mapper/redaction;
- no entity serialization.
39. Failure Mode: Entity as Request Body
Symptom
- mass assignment;
- blind merge;
- detached graph issues.
Cause
JPA entity used as request DTO.
Prevention
- request DTO;
- command object;
- explicit field application;
- expected version.
40. Failure Mode: Soft Delete Hidden by Filter
Symptom
- admin cannot find data;
- invariant ignores soft-deleted rows incorrectly;
- unique constraint mismatch.
Cause
Provider-specific filter hides rows globally.
Prevention
- explicit query methods for active vs all;
- DB constraints aligned;
- tests for admin/audit paths;
- document filters.
41. Failure Mode: Many-to-Many Hides Relationship State
Symptom
- cannot audit reviewer role/status;
- join table needs columns later;
- migration painful.
Cause
Raw @ManyToMany.
Prevention
- explicit join entity for domain relationship;
- attributes/status/version on relation.
42. Failure Mode: Wrong Collection Type
Symptom
- Set fails to remove child;
- duplicates;
- hash changes after ID assigned.
Cause
entity equals/hashCode incompatible with Set.
Prevention
- stable equality convention;
- unique constraints;
- list with explicit constraints if easier;
- avoid mutable fields in hash.
43. Failure Mode: getReference Missing Row
Symptom
- error appears at flush, not when assigning relation;
- FK violation instead of not-found response.
Cause
Using proxy reference for user-supplied ID without existence check.
Prevention
- use
findwhen absence is user-facing; - use
getReferenceonly when existence guaranteed or FK error acceptable.
44. Failure Mode: Query Returns Managed Entity for Read-Only Report
Symptom
- memory high;
- dirty checking overhead;
- accidental updates;
- slow export.
Cause
Report uses entity query.
Prevention
- DTO projection;
- native/JDBC/jOOQ chunk;
- read-only/stateless for batch if needed.
45. Failure Mode: Transaction Around Report Export
Symptom
- connection held for minutes;
- locks/snapshot pressure;
- pool exhaustion.
Cause
long read/export in one transaction.
Prevention
- async export job;
- snapshot table;
- chunk cursor;
- read-only transaction only if intentional;
- avoid holding transaction during file upload.
46. Failure Mode: Direct Broker Publish Inside Transaction
Symptom
- event published but DB rollback;
- DB commit but publish failed;
- duplicate external side effects.
Cause
service sends broker/email inside transaction.
Prevention
- transactional outbox;
- after-commit only for non-critical cache;
- idempotent consumers.
47. Failure Mode: Repository as God Object
Symptom
- repository contains command, dashboard, report, export, audit, projection;
- unclear transaction/return semantics.
Cause
no DAO/repository/query separation.
Prevention
- repository for aggregate write;
- query service for projection;
- DAO for SQL primitives;
- outbox/audit repositories separate.
48. Failure Mode: Generic Repository Hides Semantics
Symptom
savesometimes inserts/updates/merges;findAllunbounded;- lock/version unclear.
Cause
generic CRUD abstraction for complex domain.
Prevention
- intent-specific methods;
- explicit add/save/upsert;
- no unbounded findAll;
- method contracts.
49. Failure Mode: Constraint Name Not Stable
Symptom
- duplicate conflict not mapped;
- generic 500 error for unique violation.
Cause
database generated random constraint name.
Prevention
- name constraints explicitly in migrations;
- translate constraint names;
- integration tests.
50. Failure Mode: Test Uses H2 But Production PostgreSQL/Oracle/etc.
Symptom
- SQL works in test but fails production;
- locking/isolation/fetch behavior differs;
- migration gap.
Cause
test DB not representative.
Prevention
- Testcontainers/real database for data access integration;
- run migrations;
- test lock/concurrency on target DB.
51. Failure Mode: No Migration Compatibility With Entity
Symptom
- rolling deploy fails;
- old app cannot read new schema;
- new non-null column breaks old inserts.
Cause
entity and migration deployed without expand-contract.
Prevention
- backward-compatible migration;
- nullable/additive first;
- dual read/write if needed;
- contract cleanup later;
- integration tests against migration sequence.
52. Failure Mode: Overusing ORM for SQL-Heavy Workload
Symptom
- complex reporting/query code awkward and slow;
- generated SQL hard to control.
Cause
forcing JPQL/entity graph for analytics/report.
Prevention
- native SQL;
- jOOQ;
- MyBatis;
- read model;
- data pipeline/reporting store.
ORM is one tool, not the only data access mechanism.
53. Failure Mode Detection Playbook
When endpoint is slow:
- Count SQL statements.
- Identify repeated queries.
- Capture slow SQL.
- Inspect generated SQL and plan.
- Check transaction duration.
- Check persistence context size.
- Check lazy loads from mapper/serializer.
- Check count query.
- Check locks/waits.
- Check cache hit/miss if enabled.
When data is wrong:
- Check source table/version.
- Check cache/read model staleness.
- Check transaction logs/audit.
- Check outbox/inbox.
- Check merge/detached flow.
- Check bulk/native updates.
- Check constraints.
- Reproduce with integration/concurrency test.
54. ORM Review Checklist
- Entities are not API input/output.
- Read list uses DTO projection/read model.
- Query count budget exists.
- Lazy associations not accessed in serializer.
- Fetch joins are bounded.
- No multiple collection fetch join explosion.
- No blind merge from request.
- No
@Dataon entities. - Cascade only on owned children.
- Orphan removal safe.
- Bulk updates clear context and handle version.
- Mutable entities have
@Version. - Expected version in user commands.
- Cache not used for command truth.
- Constraint names stable.
- Integration tests use real DB.
- Outbox used for external events.
- Performance tests use realistic data.
55. Anti-Pattern: "Hibernate Will Optimize It"
Hibernate cannot guess your business access pattern.
Be explicit.
56. Anti-Pattern: "Just Make It EAGER"
EAGER is often permanent over-fetch.
57. Anti-Pattern: "Just Enable Cache"
Cache can turn performance bug into consistency bug.
58. Anti-Pattern: "Just Increase Pool"
Pool increase can hide slow query until DB collapses.
59. Anti-Pattern: "Entity Everywhere"
Entity as domain, DTO, request, response, cache, report. This maximizes coupling.
Separate by responsibility.
60. Mini Lab
Audit this code:
@Transactional
public CaseResponse updateCase(UUID id, CaseFileEntity request) {
CaseFileEntity existing = entityManager.find(CaseFileEntity.class, id);
existing.setTitle(request.getTitle());
existing.setStatus(request.getStatus());
existing.setAssignments(request.getAssignments());
List<DocumentEntity> docs = existing.getDocuments();
kafkaTemplate.send("case-updated", existing);
return mapper.toResponse(existing);
}
Find at least 15 issues.
Hints:
- request entity;
- mass assignment;
- status update;
- collection replacement;
- orphan removal;
- lazy documents;
- external publish inside tx;
- entity as event payload;
- response mapper lazy loads;
- no expected version;
- no audit;
- no outbox;
- no tenant scope;
- transaction/flush timing;
- merge/detached graph risk;
- no domain method.
Refactor using:
- request DTO;
- command;
- repository load for update;
- expected version;
- domain methods;
- audit;
- outbox;
- response DTO/projection.
61. Summary
ORM failure modes are predictable if you know where abstraction leaks.
You must master:
- N+1;
- hidden serializer query;
- cartesian explosion;
- pagination + fetch join;
- lazy initialization;
- eager over-fetch;
- detached entity confusion;
- blind merge overwrite;
- mass assignment;
- cascade disaster;
- orphan removal data loss;
- equals/hashCode/toString lazy bug;
- unexpected dirty update;
- auto flush before query;
- invalid intermediate state;
- bulk/native stale context;
- missing parent version;
- optimistic/pessimistic misuse;
- missing indexes/count query;
- unbounded query;
- large persistence context;
- cache staleness;
- entity API leak;
- real DB testing;
- migration compatibility.
This closes Phase 5. Part berikutnya masuk Phase 6: SQL-First dengan jOOQ dan MyBatis, dimulai dari jOOQ SQL-first mental model.
62. References
- Hibernate ORM User Guide: https://docs.hibernate.org/stable/orm/userguide/html_single/
- Jakarta Persistence Specification: https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2
- Spring Data JPA Reference: https://docs.spring.io/spring-data/jpa/reference/
- Spring Framework Transaction Management: https://docs.spring.io/spring-framework/reference/data-access/transaction.html
You just completed lesson 42 in deepen practice. 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.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.