JPA Entity Modeling for Data Access
Learn Java Data Access Pattern In Action - Part 033
Entity modeling JPA untuk data access production: identity, lifecycle, association, embeddable, value object, aggregate boundary, table mapping, versioning, audit, invariants, dan anti-pattern entity design.
Part 033 — JPA Entity Modeling for Data Access
Entity JPA bukan sekadar class yang diberi
@Entity.Entity adalah kontrak antara object model, relational schema, ORM provider, transaction boundary, lazy loading, dirty checking, cascade, identity, dan lifecycle persistence.
Entity modeling yang buruk membuat sistem terlihat cepat di awal, lalu runtuh saat:
- association menjadi N+1;
- cascade menghapus data;
- equals/hashCode merusak collection;
- merge menimpa stale state;
- lazy loading bocor ke API;
- aggregate boundary kabur;
- schema migration sulit;
- domain invariant tidak terlindungi database.
Entity modeling untuk production harus dimulai dari persistence behavior, bukan dari keinginan membuat class diagram indah.
Part ini membahas entity modeling JPA secara production-grade.
1. Core Thesis
JPA entity harus dirancang sebagai persistence model yang eksplisit.
Pertanyaan utama:
How should this object be persisted, loaded, updated, versioned, associated, and protected under transaction?
Bukan hanya:
What classes exist in domain?
Domain model dan persistence model bisa sama pada sistem sederhana, tetapi untuk sistem kompleks kamu harus sadar trade-off.
Entity modeling yang baik:
- jelas identity-nya;
- jelas lifecycle-nya;
- jelas ownership association-nya;
- minim lazy-loading surprise;
- punya optimistic version;
- tidak mengekspos entity ke API;
- tidak menyembunyikan query/report concern;
- tidak memakai cascade secara sembrono;
- selaras dengan database constraints;
- bisa diuji dengan real database.
2. Entity Is Not Automatically Aggregate
Contoh salah kaprah:
@Entity
class CaseFileEntity { ... }
@Entity
class CaseAssignmentEntity { ... }
@Entity
class CaseDocumentEntity { ... }
Lalu semua dianggap aggregate sendiri.
Dalam DDD/data consistency, aggregate root mungkin CaseFile, sedangkan assignment/document adalah child lifecycle tertentu.
Pertanyaan:
Can CaseAssignment exist independently?
Who creates/deletes it?
Can it be modified without CaseFile invariant?
Does changing it require CaseFile version?
Jika child lifecycle dikendalikan oleh parent, model repository/transaction harus mencerminkan itu.
JPA entity tidak otomatis menjawab aggregate boundary.
3. Minimal Entity Example
@Entity
@Table(name = "case_file")
public class CaseFileEntity {
@Id
@Column(name = "id", nullable = false, updatable = false)
private UUID id;
@Column(name = "tenant_id", nullable = false, updatable = false)
private UUID tenantId;
@Column(name = "case_number", nullable = false, unique = true, length = 64)
private String caseNumber;
@Column(name = "status", nullable = false, length = 32)
private String status;
@Version
@Column(name = "version", nullable = false)
private long version;
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
protected CaseFileEntity() {
// for JPA
}
}
Catatan:
protectedno-args constructor untuk JPA;idtidak updatable;- tenant tidak updatable;
- status disimpan sebagai code;
- version untuk optimistic lock;
- timestamp jelas.
4. Entity Identity
Identity harus stabil.
Common choices:
Surrogate ID
@Id
private UUID id;
Good for technical identity.
Natural key
case_number
Good as business unique key, but often not primary key.
Recommended for many systems:
id = immutable surrogate identity
case_number = unique business identifier
Why?
- business number can have format/version rules;
- surrogate ID easier for FK;
- business number can be regenerated/changed under controlled domain;
- idempotent create can use application-generated ID.
5. ID Generation Strategy
Options:
Application-generated UUID
caseFile.id = UUID.randomUUID();
entityManager.persist(caseFile);
Pros:
- ID known before insert;
- retry/idempotency easier;
- no database roundtrip for key;
- good for distributed systems.
Cons:
- random UUID index locality concerns depending DB/index;
- generated by app discipline required.
Database-generated identity/sequence
@GeneratedValue(strategy = GenerationType.IDENTITY)
Pros:
- database controls key;
- common.
Cons:
- ID known after insert/flush;
- batch insert may be affected depending strategy;
- retry after unknown commit harder unless command result stored;
- sequence gaps normal.
Choose intentionally.
6. Entity Equality
JPA entity equality is tricky.
Bad:
@Override
public boolean equals(Object o) {
return Objects.equals(id, ((CaseFileEntity) o).id);
}
If id null before persist, two new entities can compare equal incorrectly if both null.
Bad:
equals based on mutable status/title
Breaks hash collections when field changes.
Guidelines:
- prefer immutable assigned ID at construction if possible;
- use ID equality only when ID non-null;
- avoid putting transient entities in hash-based collections;
- be consistent;
- consider not overriding equals/hashCode for entities unless necessary;
- use value objects for value equality.
Example cautious:
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof CaseFileEntity that)) return false;
return id != null && id.equals(that.id);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
This style avoids mutable hash issue but has trade-offs. Team convention matters.
7. Value Object vs Entity
Value object:
- no identity;
- immutable;
- equality by value;
- owned by entity.
Example:
@Embeddable
public class CaseNumberValue {
@Column(name = "case_number", nullable = false, length = 64)
private String value;
protected CaseNumberValue() {}
public CaseNumberValue(String value) {
if (value == null || value.isBlank()) {
throw new InvalidCaseNumber(value);
}
this.value = value;
}
public String value() {
return value;
}
}
Entity:
- has identity;
- mutable lifecycle;
- persisted independently or as aggregate root/child.
Use @Embeddable for value concepts like money, period, address snapshot, case number wrapper, audit actor snapshot.
8. Embeddable Example
@Embeddable
public class DecisionSnapshot {
@Column(name = "decision_code", length = 32)
private String decisionCode;
@Column(name = "decision_reason", length = 512)
private String reason;
@Column(name = "decided_by")
private UUID decidedBy;
@Column(name = "decided_at")
private Instant decidedAt;
protected DecisionSnapshot() {}
public DecisionSnapshot(String decisionCode, String reason, UUID decidedBy, Instant decidedAt) {
this.decisionCode = Objects.requireNonNull(decisionCode);
this.reason = reason;
this.decidedBy = Objects.requireNonNull(decidedBy);
this.decidedAt = Objects.requireNonNull(decidedAt);
}
}
Embeddable works well for columns that conceptually move together.
But avoid huge embeddables that hide many nullable columns with unclear lifecycle.
9. Enum Mapping
Avoid ordinal enum mapping.
Bad:
@Enumerated(EnumType.ORDINAL)
private CaseStatus status;
If enum order changes, data corrupts.
Better:
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 32)
private CaseStatus status;
Even better for stable DB code:
@Convert(converter = CaseStatusConverter.class)
private CaseStatus status;
Converter:
@Converter(autoApply = false)
public class CaseStatusConverter implements AttributeConverter<CaseStatus, String> {
@Override
public String convertToDatabaseColumn(CaseStatus attribute) {
return attribute == null ? null : attribute.dbCode();
}
@Override
public CaseStatus convertToEntityAttribute(String dbData) {
return dbData == null ? null : CaseStatus.fromDbCode(dbData);
}
}
This decouples Java enum name from DB code.
10. Column Nullability
JPA annotations should reflect database constraints.
@Column(name = "status", nullable = false)
But remember:
JPA nullable=false is not enough.
Database must also have NOT NULL.
Use both.
Entity model documents expectation. Database enforces truth.
Avoid nullable fields with unclear meaning.
If nullable, document state:
approved_at null until status APPROVED
assigned_officer_id null when unassigned
11. Check Constraints and Entity Validation
Domain validation:
if (amount.signum() < 0) throw ...
Database check:
check (amount >= 0)
JPA Bean Validation:
@PositiveOrZero
private BigDecimal amount;
Layers complement:
- domain gives good error and early validation;
- Bean Validation integrates lifecycle;
- DB constraint protects final truth.
Do not rely only on entity annotations for critical invariants.
12. Version Field
Every mutable aggregate root should usually have @Version.
@Version
@Column(name = "version", nullable = false)
private long version;
Benefits:
- optimistic locking;
- stale update detection;
- event aggregate version;
- projection ordering;
- audit correlation;
- conflict response.
If child changes should increment parent version, design explicitly. JPA does not always increment parent when child table changes unless parent entity dirty or forced increment.
13. Parent Version and Child Changes
Case assignment insert may not update case_file.version.
If aggregate version should represent any aggregate change:
Options:
- explicitly update parent timestamp/version;
OPTIMISTIC_FORCE_INCREMENT;- domain method changes parent field;
- database trigger increments version;
- save parent through repository after child mutation.
Example:
entityManager.lock(caseFileEntity, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
Use carefully. Test version increments on child modifications.
14. Timestamp Fields
Common:
created_at
updated_at
Options:
- application sets via clock;
- database default/trigger;
- JPA lifecycle callbacks;
- Hibernate annotations.
For domain/audit consistency, prefer explicit application timestamp from command when meaningful.
For technical update timestamp, JPA callbacks can be acceptable.
Be clear:
created_at = persistence creation time
approved_at = domain decision time
published_at = outbox publish time
Do not mix semantics.
15. Lifecycle Callbacks
@PrePersist
void prePersist() {
createdAt = Instant.now();
updatedAt = createdAt;
}
@PreUpdate
void preUpdate() {
updatedAt = Instant.now();
}
Useful for technical timestamps.
Risks:
- hidden time source;
- hard to test;
- no actor/reason;
- no command context;
- callbacks cannot reliably do domain workflow;
- external side effects forbidden.
Do not send events/email from entity callbacks.
16. Association Modeling Principles
Before mapping association, ask:
Do I need object navigation, or just foreign key?
Who owns lifecycle?
What is cardinality?
Will this be loaded often?
Could lazy loading cause N+1?
Will cascade be safe?
Is child collection bounded?
Do not map every foreign key as object association automatically.
Sometimes this is better:
@Column(name = "assigned_officer_id")
private UUID assignedOfficerId;
than:
@ManyToOne(fetch = FetchType.LAZY)
private OfficerEntity assignedOfficer;
especially if officer belongs to another bounded context/service/module.
17. ManyToOne Default Fetch Problem
JPA default for @ManyToOne is eager unless specified.
Always be explicit:
@ManyToOne(fetch = FetchType.LAZY, optional = true)
@JoinColumn(name = "assigned_officer_id")
private OfficerEntity assignedOfficer;
Eager ManyToOne can accidentally load reference tables repeatedly.
But lazy association has proxy behavior. Decide intentionally.
For many read screens, DTO projection join is better than entity association navigation.
18. OneToMany Collection
@OneToMany(
mappedBy = "caseFile",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<CaseAssignmentEntity> assignments = new ArrayList<>();
Questions:
- Is collection bounded?
- Is parent owner?
- Should removing from list delete row?
- Can assignment exist independently?
- Will loading case load assignments?
- Are modifications through parent only?
If collection can be huge, do not model as ordinary List on entity for command paths.
19. Avoid Many-to-Many for Domain
JPA @ManyToMany hides join table as implementation detail.
For domain systems, join table often has attributes:
created_at
created_by
role
status
ended_at
reason
Use explicit entity:
CaseReviewerEntity
instead of direct many-to-many.
Bad for complex domain:
@ManyToMany
private Set<ReviewerEntity> reviewers;
Better:
@OneToMany(mappedBy = "caseFile")
private List<CaseReviewerEntity> reviewers;
20. Association Ownership
In JPA, owning side controls foreign key.
Example:
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "case_id", nullable = false)
private CaseFileEntity caseFile;
Child owns FK.
Parent:
@OneToMany(mappedBy = "caseFile")
private List<CaseAssignmentEntity> assignments;
If you add child only to parent collection but do not set child parent, FK may not persist correctly.
Use helper method:
public void addAssignment(CaseAssignmentEntity assignment) {
assignments.add(assignment);
assignment.setCaseFile(this);
}
Keep both sides in sync.
21. Bidirectional Association Caveat
Bidirectional association can be useful but increases complexity:
- both sides must be synchronized;
- equals/hashCode issues;
- serialization recursion;
- larger object graph;
- lazy loading surprises.
Use unidirectional association or FK field if navigation not needed.
22. Cascade Design
Cascade should reflect lifecycle ownership.
Good:
CaseFile -> CaseAssignment
if assignment cannot exist without case and is only changed through case aggregate.
Dangerous:
CaseFile -> Officer
If officer is shared reference data, cascade remove would be disastrous.
Rule:
Never cascade remove to shared reference/aggregate roots.
23. Orphan Removal Design
orphanRemoval = true means removing child from collection deletes it.
Good for owned child:
CaseFile owns temporary draft line items.
Danger for historical/audit rows:
Case actions/audit must never disappear just because collection replaced.
Do not use orphan removal on audit/history collections.
24. Audit Rows Are Not Normal Children
Audit log should usually be append-only.
Do not map audit as cascade/orphan-removable child collection.
Better:
auditRepository.append(...)
or dedicated CaseAuditEntity persisted explicitly.
If entity has List<AuditEntity>, keep it read-only or avoid mapping collection.
25. Outbox Rows Are Infrastructure, Not Entity Child
Outbox event belongs to transaction, but not usually as child association navigated from aggregate.
Use outbox repository/entity persisted explicitly.
entityManager.persist(outboxEventEntity);
Do not model:
caseFile.getOutboxEvents()
as domain association.
26. Entity Constructor Design
JPA needs no-arg constructor, but domain construction should be controlled.
protected CaseFileEntity() {}
public static CaseFileEntity createNew(
UUID id,
UUID tenantId,
String caseNumber,
Instant now
) {
CaseFileEntity entity = new CaseFileEntity();
entity.id = id;
entity.tenantId = tenantId;
entity.caseNumber = caseNumber;
entity.status = CaseStatus.DRAFT.dbCode();
entity.version = 0;
entity.createdAt = now;
entity.updatedAt = now;
return entity;
}
Avoid public setters for everything if entity is used as domain model.
27. Setter Discipline
If entity is persistence-only, setters may be package-private and mapper uses them.
If entity has domain behavior, prefer methods:
public void approve(UserId actor, String reason, Instant now) {
if (!status.equals("UNDER_REVIEW")) {
throw new InvalidTransition();
}
status = "APPROVED";
approvedAt = now;
updatedAt = now;
}
Avoid arbitrary:
setStatus("APPROVED")
from everywhere.
28. Entity as Persistence Model Only
If separate domain model:
@Entity
class CaseFileEntity {
// fields + persistence mapping only
}
Domain:
public final class CaseFile {
public void approve(...) { ... }
}
Mapper translates.
This reduces risk of JPA proxies/lazy loading in domain.
Cost: mapping code.
Good for complex domain.
29. Entity as Domain Model
If using JPA entity as domain object:
@Entity
class CaseFileEntity {
public void approve(...) { ... }
}
Guidelines:
- do not expose entity to API;
- keep transaction in service;
- avoid lazy loading inside domain method unless explicitly loaded;
- use
@Version; - keep cascade safe;
- avoid callbacks for business workflow;
- test with real DB.
This hybrid is common but requires discipline.
30. Table Mapping Naming
Be explicit.
@Entity
@Table(
name = "case_file",
uniqueConstraints = {
@UniqueConstraint(
name = "uq_case_file_tenant_case_number",
columnNames = {"tenant_id", "case_number"}
)
},
indexes = {
@Index(
name = "ix_case_file_tenant_status_updated",
columnList = "tenant_id, status, updated_at"
)
}
)
Still create/manage constraints via migrations. JPA annotations are documentation and may support schema generation in tests, but production migrations should be explicit.
31. Unique Constraint Naming
Constraint names matter for error translation.
Migration:
alter table case_file
add constraint uq_case_file_tenant_case_number
unique (tenant_id, case_number);
Translator maps:
uq_case_file_tenant_case_number -> DuplicateCaseNumber
Do not rely on random generated constraint names.
32. Foreign Key Mapping
Entity association:
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(
name = "case_id",
nullable = false,
foreignKey = @ForeignKey(name = "fk_case_assignment_case")
)
private CaseFileEntity caseFile;
Or FK field:
@Column(name = "case_id", nullable = false)
private UUID caseId;
Association is useful for navigation/cascade. FK field is simpler for write/query code.
Do not model association just because FK exists.
33. Reference Data
Reference data like status/reason code can be:
- enum/code in column;
- FK to reference table;
- cached lookup;
- association.
If using association to reference entity, avoid cascade remove/persist.
Often for stable code lists:
status_code text check (...)
or enum converter is enough.
34. Large Text/LOB Fields
Avoid loading large fields by default in entity used frequently.
Example:
document_content
large_payload
raw_json
Options:
- separate table/entity;
- lazy basic field if provider supports and configured;
- DTO query excluding it;
- object storage reference;
- dedicated repository for blob.
Do not put huge blob in common aggregate root if every load fetches it.
35. JSON Columns
JSON can be mapped as string/custom type.
Use for:
- flexible payload;
- outbox event;
- command result;
- external snapshot.
Caution:
- schema validation;
- indexing;
- migrations;
- query complexity;
- object mapping errors;
- size.
For core relational fields, prefer columns.
36. Money and Numeric
Use BigDecimal, not floating point.
@Column(name = "amount", nullable = false, precision = 19, scale = 2)
private BigDecimal amount;
Consider embeddable Money:
@Embeddable
class MoneyEmbeddable {
@Column(name = "amount", precision = 19, scale = 2)
BigDecimal amount;
@Column(name = "currency", length = 3)
String currency;
}
Database constraints should enforce currency/amount validity.
37. Time Mapping
Use Instant for machine timestamp.
Use LocalDate for date-only business concept.
Use timezone-aware conversion at boundaries.
Do not store user-local date-time without timezone semantics.
Columns:
@Column(name = "approved_at", nullable = true)
private Instant approvedAt;
Document time source.
38. Soft Delete
Soft delete field:
@Column(name = "deleted_at")
private Instant deletedAt;
Query must exclude deleted rows.
Options:
- explicit predicates in queries;
- ORM filter/provider-specific annotation;
- repository methods.
Be careful:
- unique constraints must consider active rows if needed;
- associations may include deleted children;
- admin queries need include deleted;
- hard delete semantics still needed for cleanup maybe.
Do not hide soft delete too deeply if it affects correctness.
39. Entity Inheritance
JPA inheritance strategies:
- single table;
- joined;
- table per class.
They add complexity.
Use only if polymorphism is truly needed.
Often simpler:
type column + composition
or separate tables with explicit repository methods.
Inheritance can complicate queries, constraints, migrations, and performance.
40. Entity Versioning vs Audit History
@Version is concurrency control, not audit history.
Audit history needs separate table:
case_audit_log
case_status_history
Do not expect entity version to explain who/why/when.
Version answers:
Has this row changed since I read it?
Audit answers:
What happened, by whom, why, and when?
41. Entity Validation and Database Truth
Entity method can validate:
if (status != UNDER_REVIEW) throw ...
Database should still protect:
- unique constraints;
- FK constraints;
- not null;
- check constraints;
- optimistic version;
- conditional update if using SQL.
Entity validation alone is not enough under concurrency.
42. Entity Modeling and Query Model Separation
Do not add fields/associations to entity only for dashboard.
Example:
@Transient
private String assignedOfficerName;
This is projection concern.
Use DTO/read model.
Entity should model persistence state relevant to aggregate, not every view field.
43. Entity Modeling and Batch
JPA entity model may be bad for massive batch.
If batch updates millions of rows:
- use JDBC/jOOQ bulk/chunk;
- clear persistence context;
- avoid loading full entity graph;
- use stateless session if provider supports.
Entity model is not mandatory for every data access operation.
44. Entity Modeling and Multi-Tenancy
Tenant field:
@Column(name = "tenant_id", nullable = false, updatable = false)
private UUID tenantId;
All queries must scope by tenant.
Options:
- explicit tenant predicate;
- Hibernate filter;
- multi-tenant support;
- database row-level security.
Even with framework support, tests should prove tenant isolation.
45. Entity Modeling and Optimistic Lock Events
If outbox event includes aggregate version, ensure version known.
With JPA, version may increment at flush. If you build event before flush, version may still be old.
Options:
- use current version + 1 by convention;
- flush before event creation carefully;
- create event after flush but before commit;
- use database-generated version readback;
- store event with expected new version from domain.
Be explicit. Test it.
46. Entity Modeling and Generated Values
If database default sets fields:
created_at default now()
Entity may not know value until refresh/returning.
For command result/outbox payload, app may need value immediately.
Options:
- set in app;
- flush and refresh;
- use generated annotations if provider supports;
- query after insert.
Avoid relying on unknown DB-generated values for response unless handled.
47. Entity Modeling and Immutable Fields
Use updatable = false for fields that should never change.
@Column(name = "tenant_id", nullable = false, updatable = false)
private UUID tenantId;
But database should also enforce if critical, or at least application never updates.
JPA annotation helps prevent accidental update SQL but is not full security boundary.
48. Entity Modeling Review Checklist
- Entity has stable identity.
- ID generation strategy chosen intentionally.
-
@Versionexists for mutable aggregate root. - Column nullability matches DB constraints.
- Enum mapping is stable, not ordinal.
- Associations are intentionally mapped.
-
ManyToOnefetch type explicit. -
ManyToManyavoided for rich domain. - Cascade matches lifecycle ownership.
- Orphan removal safe.
- Audit/outbox not modeled as accidental child graph.
- Large fields separated or lazily handled.
- Entity not exposed to API.
- Tenant scope modeled/enforced.
- Equality/hashCode convention safe.
- Constraints have stable names.
- Migration compatibility considered.
- Tests cover mapping, version, cascade, tenant, constraints.
49. Anti-Pattern: Entity Mirrors API JSON
Entity has fields solely for frontend response.
Use DTO projection/response.
50. Anti-Pattern: @ManyToMany Everywhere
Join table with no entity hides important lifecycle attributes.
Use explicit join entity for domain associations.
51. Anti-Pattern: CascadeType.ALL Everywhere
Can persist/delete huge/shared graph accidentally.
Cascade only for owned children.
52. Anti-Pattern: Ordinal Enum
Changing enum order corrupts meaning.
Use string/code converter.
53. Anti-Pattern: Entity Without Version
Mutable aggregate without optimistic lock invites lost update.
Use @Version or explicit concurrency strategy.
54. Anti-Pattern: Returning Entity From Controller
Leaky, lazy, unsafe.
Return response DTO.
55. Anti-Pattern: Collection Replacement With Orphan Removal
Can delete children unexpectedly.
Apply explicit add/remove operations.
56. Mini Lab
Model JPA entities for:
CaseFile aggregate:
- id
- tenant
- case number
- status
- version
- assignments
- reviewers
- audit
- outbox
- documents
Questions:
- Which classes are aggregate root entities?
- Which child entities are owned by CaseFile?
- Which records should not be child collection?
- Which associations should be FK field only?
- Which fields need
@Version? - Which unique constraints are required?
- What cascade is safe?
- Should documents store blob or object key?
- How is tenant enforced?
- What DTO/read model is separate?
57. Summary
JPA entity modeling is persistence design.
You must master:
- entity identity;
- ID generation strategy;
- equality/hashCode;
- value object/embeddable;
- enum converter;
- nullability and constraints;
- optimistic version;
- parent version for child changes;
- timestamp semantics;
- association ownership;
- explicit fetch type;
- cascade/orphan removal;
- avoiding many-to-many for rich domain;
- audit/outbox separation;
- soft delete;
- tenant scope;
- generated values;
- entity/domain separation;
- read projection separation;
- entity review checklist.
Part berikutnya membahas EntityManager and Persistence Context lebih dalam: managed/detached/transient, persist, merge, remove, flush, clear, find, getReference, lock, refresh, and operational lifecycle.
58. References
- Jakarta Persistence Specification: https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2
- Hibernate ORM User Guide: https://docs.hibernate.org/stable/orm/userguide/html_single/
- Spring Data JPA Reference: https://docs.spring.io/spring-data/jpa/reference/
- Oracle Java SE
UUID: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/UUID.html
You just completed lesson 33 in build core. 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.