Data Access Contract Design
Learn Java Data Access Pattern In Action - Part 032
Desain kontrak method data access Java: naming, return type, nullability, Optional, collection, stream, pagination, cursor, update count, error semantics, transaction expectation, locking, idempotency, dan API review rubric.
Part 032 — Data Access Contract Design
Banyak bug data access bukan berasal dari SQL yang salah.
Banyak bug berasal dari kontrak method yang ambigu:
User getUser(String id); List<Order> findOrders(Filter filter); boolean save(Entity e); void updateStatus(UUID id, String status); Stream<Row> streamAll();Apakah
getUserreturn null?Apakah list bounded?
Apakah
saveinsert atau update?Apakah
falseberarti conflict atau not found?Siapa yang menutup
Stream?Apakah method butuh transaction?
Apakah lock diambil?
Kontrak method data access harus membuat correctness terlihat dari signature dan dokumentasi.
Part ini membahas desain kontrak data access yang production-grade.
1. Core Thesis
Data access contract harus menghapus ambiguitas.
Setiap method DAO/repository/query service harus jelas tentang:
- intent;
- cardinality;
- transaction expectation;
- locking behavior;
- return semantics;
- nullability;
- collection bound;
- pagination;
- error behavior;
- update count;
- idempotency;
- consistency;
- ownership.
Good contract reduces bugs before code runs.
2. Bad Contract Example
CaseFile get(String id);
Ambiguities:
- raw string ID?
- tenant scoped?
- returns null if not found?
- throws if not found?
- locks row?
- loads full aggregate or row?
- includes child collections?
- requires transaction?
- may lazy load later?
- maps deleted rows?
- what if multiple rows due data corruption?
Better:
Optional<CaseFile> loadForApproval(CaseFileId id);
or DAO:
Optional<CaseFileRow> findByTenantAndId(
Connection connection,
TenantId tenantId,
CaseFileId id
);
3. Naming: find, get, load, exists
Recommended semantics:
| Prefix | Suggested Meaning |
|---|---|
find... | optional lookup, may return empty |
get... | required lookup, throws if absent |
loadFor... | loads shape needed for specific use case |
exists... | existence check only, beware race |
search... | filter/paginated query |
readAfter... | cursor/chunk read |
count... | count query |
insert... | create new row, duplicate is error/conflict |
update... | modify existing row, count checked |
delete... | hard delete, use carefully |
mark... | state/flag transition |
claim... | take work/lock/lease ownership |
append... | immutable/event/audit insert |
Be consistent across codebase.
4. find vs get
find:
Optional<CaseFile> findById(CaseFileId id);
Caller handles absence.
get:
CaseFile getById(CaseFileId id);
Throws CaseNotFound.
Use get only when absence is exceptional in that context.
Avoid:
CaseFile findById(...) // returns null
5. loadFor... Intent Methods
Command-specific load:
Optional<CaseFile> loadForApproval(CaseFileId id);
Optional<CaseFile> loadForAssignment(CaseFileId id);
Optional<CaseFile> loadForClosure(CaseFileId id);
Benefits:
- communicates fetch shape;
- avoids partial aggregate misuse;
- avoids over-fetching;
- supports use-case-specific lock/fetch plan;
- improves review.
If method locks, include it:
Optional<CaseFile> loadForAssignmentWithLock(CaseFileId id);
6. Avoid Ambiguous save
save can mean:
- insert;
- update;
- upsert;
- merge detached object;
- persist dirty state;
- replace entire aggregate.
Prefer explicit:
void add(CaseFile newCase);
void save(CaseFile existingCase);
void updateStatus(CaseStatusUpdate update);
void upsertReadModel(CaseDashboardRow row);
If using save, document semantics:
Updates an existing aggregate using optimistic version.
Throws AggregateNotFound or OptimisticConflict if no row updated.
Does not insert.
7. Insert Contract
void insert(Connection connection, CaseFileInsertRow row);
Contract:
Creates a new case row.
Requires caller transaction.
Throws DuplicateCaseNumber if unique case number violated.
Throws DuplicateAggregateId if id already exists.
Does not update existing row.
Do not silently upsert unless method name says upsert.
8. Update Contract
void updateStatusWithExpectedVersion(
Connection connection,
TenantId tenantId,
CaseFileId caseId,
CaseStatus newStatus,
long expectedVersion
);
Contract:
Updates exactly one row matching tenant/id/version.
If no row updated, throws OptimisticConflict or scoped not found.
If more than one row updated, throws invariant violation.
Return type could be void if method enforces count internally.
Or:
int updateStatusReturningAffectedRows(...)
if caller decides.
Do not ignore affected rows.
9. Delete Contract
Hard delete method should be explicit:
void hardDeleteDraftById(Connection connection, TenantId tenantId, CaseFileId id);
Soft transition:
void markArchived(CaseFile caseFile);
Avoid generic:
delete(id);
for domain objects.
Deletion semantics must answer:
- hard or soft?
- allowed state?
- cascades?
- audit?
- idempotent?
- expected row count?
- tenant scoped?
- outbox event?
10. Return Type: Optional
Use Optional<T> for optional single result.
Good:
Optional<CaseFileRow> findById(...);
Avoid:
Optional<List<T>>;Optionalfield in JPA entity;- returning
nullinstead ofOptional.empty; Optionalparameter unless it makes API clearer.
For query object, Optional fields are okay if absence semantics clear.
11. Return Type: Collection
If returning List<T>, contract must state boundedness.
Bad:
List<CaseFile> findOpenCases();
Better:
List<CaseFileRow> findOpenCases(PageRequest page);
List<BackfillCandidateRow> readOpenCasesAfter(CaseCursor cursor, int limit);
If collection inherently small, document:
List<CaseReviewer> findActiveReviewers(CaseFileId id);
But still know worst-case.
12. Empty Collection vs Null
Return empty collection, not null.
List<CaseActionRow> actions = actionDao.findRecent(...);
If none:
List.of()
This eliminates null checks.
13. Return Type: Page vs Slice
Page usually includes total count.
public record Page<T>(
List<T> items,
int pageNumber,
int pageSize,
long totalItems
) {}
Slice only says whether next page exists.
public record Slice<T>(
List<T> items,
boolean hasNext,
Optional<Cursor> nextCursor
) {}
Use Slice when count is expensive.
Do not run expensive count query just because UI wants pagination component. Challenge requirement.
14. Cursor Return Type
For keyset pagination:
public record CursorSlice<T, C>(
List<T> items,
Optional<C> nextCursor
) {}
Example:
CursorSlice<CaseDashboardRow, CaseDashboardCursor> search(CaseDashboardQuery query);
Cursor must be opaque or well-defined.
API may encode cursor as token; internal query uses typed cursor.
15. Return Type: Stream
Returning Stream<T> from data access is dangerous.
If you do:
Stream<CaseExportRow> streamExportRows(CaseExportQuery query);
Contract must state:
Caller must close stream.
Stream holds database resources until closed.
Stream must not be parallelized.
Stream must be consumed within transaction/request scope.
Usage:
try (Stream<CaseExportRow> rows = exportQuery.stream(query)) {
rows.forEach(writer::write);
}
For most cases, prefer callback or chunked cursor.
16. Callback for Streaming
void scanExportRows(CaseExportQuery query, CaseExportRowHandler handler);
Contract:
DAO owns resource lifecycle.
Handler is called while connection/result set are open.
Handler must not perform slow external I/O.
Chunking often better:
List<CaseExportRow> readAfter(CaseExportCursor cursor, int limit);
17. Return Type: Boolean
Boolean often hides information.
Bad:
boolean updateStatus(...);
What does false mean?
- not found?
- stale version?
- unauthorized?
- invalid state?
- duplicate?
- no-op idempotent?
Better:
UpdateStatusResult updateStatus(...);
or throw semantic exception.
Example result:
public sealed interface UpdateStatusResult {
record Updated(long newVersion) implements UpdateStatusResult {}
record NotFound() implements UpdateStatusResult {}
record VersionConflict(long currentVersion) implements UpdateStatusResult {}
record InvalidState(CaseStatus currentStatus) implements UpdateStatusResult {}
}
Use boolean only for truly binary operations where false semantics obvious.
18. Return Type: Count
Count can be useful for bulk/maintenance.
int markExpiredAssignments(Connection connection, Instant now, int limit);
Contract:
Returns number of rows marked expired.
May be 0 when no eligible rows.
Does not tell which rows.
Does not append per-row audit.
If caller needs row IDs/events, return rows or use returning/two-step pattern.
19. Update Result Object
For complex update:
public record ClaimResult<T>(
List<T> claimedItems,
int requestedLimit,
boolean mayHaveMore
) {}
Outbox claim:
ClaimResult<OutboxEventRow> claimNextBatch(
String workerId,
Instant staleBefore,
int limit
);
Clearer than returning raw list when ownership semantics matter.
20. Nullability
Java lacks built-in non-null enforcement. Use conventions/tools:
- no method returns null unless explicitly annotated/legacy;
- use
Optionalfor optional single; - use empty collection;
- validate constructor arguments;
- use
@Nullableonly intentionally; - use records for immutable DTOs;
- prefer domain value types.
Document nullability.
Bad:
String getOfficerName(); // nullable?
Better:
Optional<String> assignedOfficerName();
or nullable annotation if DTO framework needs.
21. Domain IDs in Signatures
Bad:
findById(UUID id)
Better:
findById(CaseFileId id)
Use value types:
public record CaseFileId(UUID value) {
public CaseFileId {
Objects.requireNonNull(value);
}
}
Prevents passing wrong ID type.
DAO may convert to UUID internally.
22. Tenant Scope in Contract
Tenant should be explicit.
Optional<CaseFileRow> findById(
Connection connection,
TenantId tenantId,
CaseFileId id
);
If using tenant context:
Optional<CaseFile> findById(CaseFileId id);
document:
Uses CurrentTenant; throws MissingTenantContext if absent.
For admin cross-tenant method, name it:
findByIdAcrossTenants(AdminScope scope, CaseFileId id);
Make elevated behavior obvious.
23. Transaction Expectation
DAO method should state:
Requires active transaction?
Can run outside transaction?
Does it commit?
Does it lock?
Does it participate in caller transaction?
Example Javadoc:
/**
* Participates in caller transaction.
* Does not commit/rollback or close caller connection.
* Should be called inside command transaction because audit/outbox
* must commit with status update.
*/
void updateStatusWithVersion(Connection connection, CaseStatusUpdate update);
If method opens own connection, name/placement should make it clear.
24. Locking in Contract
Locking method name:
findByIdForUpdate
loadForAssignmentWithLock
claimNextBatchSkipLocked
tryLockNowait
Contract:
Acquires row lock until transaction ends.
May block up to configured lock timeout.
Throws LockNotAvailable if nowait/timeout.
Do not hide locks in ordinary find method.
25. Consistency in Contract
Read method may be:
- strongly consistent from primary;
- eventually consistent read model;
- snapshot at cutoff;
- stale cache;
- replica read.
Name or docs should indicate.
Examples:
CaseDetailView getCurrentDetail(CaseFileId id);
CaseDashboardRow findInDashboardReadModel(CaseFileId id);
CaseReportSnapshot readFromSnapshot(ReportRunId id);
If query reads replica/read model, command side must not use it for truth.
26. Idempotency in Contract
Write method should clarify idempotency.
AppendOutboxResult appendIfAbsent(OutboxEvent event);
Result:
public enum AppendOutboxResult {
INSERTED,
DUPLICATE_SAME_EVENT
}
For command:
ApproveCaseResult handle(ApproveCaseCommand command);
Command object includes command ID.
Contract:
Same command ID and payload returns same result.
Same command ID with different payload throws IdempotencyKeyConflict.
27. Error Semantics
Avoid leaking raw persistence exceptions beyond boundary.
Define categories:
- not found;
- conflict;
- duplicate;
- invalid state;
- authorization/scoped not found;
- retryable transaction failure;
- data access unavailable;
- mapping/invariant violation;
- programmer/migration bug.
Each method should document expected semantic exceptions.
28. Not Found vs Unauthorized
For security, method may intentionally collapse.
Optional<CaseFile> findVisibleToUser(UserScope scope, CaseFileId id);
If empty:
not found or not visible
This can be good.
But internal method should not accidentally hide authorization bugs.
Name it:
findVisible...
not generic findById.
29. Conflict Semantics
Conflict types:
- optimistic version conflict;
- state transition conflict;
- unique constraint conflict;
- idempotency key conflict;
- lock conflict;
- capacity conflict.
Do not throw generic ConflictException everywhere if caller needs different response.
Use typed exceptions/results.
30. Exception vs Result
Use exception when:
- operation cannot complete normally;
- failure is exceptional for caller flow;
- transaction should rollback;
- semantic error maps to API error.
Use result when:
- failure is expected branch;
- caller likely handles multiple outcomes;
- no stack trace needed;
- query/claim operation naturally has status.
Example result for work claim is good.
Example duplicate case number may be exception.
Be consistent.
31. Checked vs Runtime Exceptions
Java checked exceptions can express contract but often pollute layers.
Many enterprise apps use runtime domain/data access exceptions.
Important:
- transaction rollback rules must include them;
- exceptions are typed enough;
- not swallowed.
If using checked business exceptions, configure rollback if mutation happened before throw.
32. Method Naming for Scope
Bad:
searchCases(...)
Better:
searchVisibleCases(...)
searchCasesForTenant(...)
searchCasesForSupervisorDashboard(...)
searchCasesAcrossTenantsForAdmin(...)
Scope is correctness and security.
33. Method Naming for Projection
Bad:
findCases(...)
Better:
searchDashboardRows(...)
findDetailHeader(...)
readExportRowsAfter(...)
findTimelineRows(...)
Return shape visible in name.
34. Method Naming for Mutations
Bad:
process(...)
update(...)
save(...)
Better:
approveWithExpectedVersion(...)
markAssignmentExpired(...)
reserveOfficerCapacity(...)
appendAuditRecord(...)
completeCommandResult(...)
markOutboxPublishedIfClaimedBy(...)
Use domain/persistence action words.
35. Contract for Batch Method
void insertBatch(Connection connection, List<CaseAuditRow> rows);
Contract:
No-op for empty list.
Caller owns transaction.
All rows are attempted as one JDBC batch.
Throws DuplicateAuditRecord on unique command/action conflict.
On SQLException, caller transaction should rollback.
Requires each row to have stable ID.
Does not chunk internally.
Or if method chunks internally, say so.
36. Contract for Bulk Update
int markExpiredAssignmentsBefore(
Connection connection,
Instant cutoff,
int limit
);
Contract:
Updates at most limit rows.
Returns affected row count.
Does not write audit/outbox.
Intended for technical cleanup only.
Order unspecified unless documented.
If domain-significant, method should return IDs or insert audit/outbox.
37. Contract for Claim Method
List<OutboxEventRow> claimNextBatch(
Connection connection,
WorkerId workerId,
Instant staleBefore,
int limit
);
Contract:
Claims up to limit unpublished events.
Uses skip-locked/lease semantics.
Rows returned are marked claimed by workerId in same transaction.
Caller must commit before publishing if using claim-then-publish.
May return empty list.
This is far more precise than findUnpublished.
38. Contract for Mark Published
void markPublishedIfClaimedBy(
Connection connection,
OutboxEventId eventId,
WorkerId workerId,
Instant publishedAt
);
Contract:
Updates exactly one row where event is claimed by worker.
If no row updated, throws OutboxClaimLost.
Idempotency of external publish is handled by event key/consumer.
Update count is part of ownership correctness.
39. Contract for Read Model Upsert
ProjectionApplyResult applyCaseSnapshot(
Connection connection,
CaseDashboardSnapshot snapshot
);
Result:
public enum ProjectionApplyResult {
INSERTED,
UPDATED,
IGNORED_OLD_OR_DUPLICATE
}
Contract:
Applies only if snapshot sourceVersion is newer.
Duplicate/old event no-ops.
This documents eventual consistency behavior.
40. Contract for Rebuild
CursorSlice<CaseDashboardSnapshot, CaseFileCursor> readSnapshotsAfter(
CaseFileCursor cursor,
int limit
);
Contract:
Reads source snapshots ordered by case ID.
Used for read model rebuild.
Limit must be <= configured max.
Rows reflect current source state, not historical event stream.
Rebuild query differs from live dashboard query.
41. Contract for Count
long countOpenCasesForTenant(TenantId tenantId);
Contract:
Counts current source rows.
May be expensive for large tenant.
Does not include archived cases.
Read committed consistency.
If count approximate:
ApproximateCount estimateOpenCases(...)
Do not name approximate as exact.
42. Contract for exists
exists is often race-prone.
boolean existsActivePrimaryAssignment(CaseFileId id);
Contract should warn:
For display/pre-check only.
Do not use as sole correctness guard for insert.
Unique constraint enforces final invariant.
For command, prefer insert with constraint handling or lock.
43. Contract for findForUpdate
Optional<CaseFileRow> findByIdForUpdate(
Connection connection,
TenantId tenantId,
CaseFileId id
);
Contract:
Locks row if found.
If not found, no lock.
Requires active transaction.
May block.
Lock released on commit/rollback.
If no active transaction and autocommit true, lock may release immediately depending DB. Therefore require transaction.
44. Contract for tryLock
TryLockResult<CaseFileRow> tryFindByIdForUpdateNowait(...);
Result:
sealed interface TryLockResult<T> {
record Locked<T>(T row) implements TryLockResult<T> {}
record NotFound<T>() implements TryLockResult<T> {}
record AlreadyLocked<T>() implements TryLockResult<T> {}
}
This is clearer than exception if lock conflict is expected branch.
45. Contract for API Query Freshness
CaseDetailView getCaseDetail(CaseFileId id);
Docs:
Reads from primary OLTP tables.
Strongly consistent after committed command.
Does not include eventually consistent search index data.
Dashboard:
CaseDashboardPage searchDashboard(...);
Docs:
Reads from case_dashboard_read_model.
Eventually consistent with source case state.
Projection target lag < 5 seconds.
Freshness should not be tribal knowledge.
46. Contract and Documentation Style
For critical methods, include Javadoc.
/**
* Reserves one active-case capacity slot for an officer.
*
* Transaction:
* - Participates in caller transaction.
*
* Concurrency:
* - Uses atomic conditional update on officer_workload.
* - Throws OfficerCapacityExceeded if active count is already at max.
*
* Idempotency:
* - Not idempotent by itself. Caller must guard with command idempotency.
*
* Side effects:
* - Does not write audit or outbox.
*/
void reserveCapacity(Connection connection, OfficerId officerId);
This is not noise for critical methods. It prevents misuse.
47. Contract and Type Design
Use types to encode constraints:
public record PageLimit(int value) {
public PageLimit {
if (value < 1 || value > 200) {
throw new InvalidQuery("limit must be 1..200");
}
}
}
Then method:
Slice<Row> search(Filter filter, PageLimit limit);
Invalid limit cannot reach DAO.
48. Contract and Value Objects
public record CaseNumber(String value) {
public CaseNumber {
if (value == null || value.isBlank()) {
throw new InvalidCaseNumber(value);
}
}
}
This avoids passing raw invalid strings.
DAO maps CaseNumber.value() to SQL.
49. Contract and Time Range
public record TimeRange(Instant fromInclusive, Instant toExclusive) {
public TimeRange {
if (!fromInclusive.isBefore(toExclusive)) {
throw new InvalidTimeRange();
}
}
}
Method:
List<CaseExportRow> findDecidedCases(TimeRange decidedAt, PageLimit limit);
Clear boundary, no end-of-day ambiguity.
50. Contract and Command Object
Instead of many parameters:
void approve(UUID caseId, UUID actorId, String reason, UUID commandId, Instant now);
Use command:
ApproveCaseCommand command;
Benefits:
- stable idempotency key;
- payload hash;
- actor/reason/time;
- validation;
- easier evolution;
- less parameter order bug.
Data access DAO may still use row/update objects, but use case contract should be command-oriented.
51. Contract and Row Object
DAO update object:
public record CaseStatusUpdateRow(
UUID tenantId,
UUID caseId,
String newStatus,
long expectedVersion,
Instant updatedAt
) {}
Avoid long parameter lists in DAO too.
52. Contract and Binary Compatibility
Public APIs/libraries need compatibility. Internal app code can evolve faster.
For internal data access, prefer clarity over premature binary compatibility.
For shared module repository interface, version carefully.
53. Contract and Overloading
Avoid overloaded data access methods with subtle differences.
Bad:
findById(UUID id)
findById(UUID id, boolean lock)
findById(UUID id, boolean lock, boolean includeDeleted)
Better:
findById(id)
findByIdForUpdate(id)
findIncludingArchivedById(id)
Boolean flags hide behavior.
54. Avoid Boolean Parameters
Bad:
findCases(filter, true, false)
Better:
CaseSearchOptions options = new CaseSearchOptions(
IncludeArchived.NO,
LockMode.NONE
);
or separate methods.
Boolean parameter names disappear at call site.
55. Contract and Time Source
If method writes timestamp:
Option A: caller passes Instant now.
markApproved(caseId, now);
Option B: repository uses injected clock.
Be consistent.
Passing now from command improves idempotency/retry consistency.
For DB now(), document database time source.
56. Contract and Generated Keys
Method returning generated key:
CaseFileId insertAndReturnId(CaseFileInsert row);
Contract:
- database generates ID;
- not idempotent unless caller command result maps ID;
- may not be safe for retry after unknown commit.
For retry-safe create, prefer caller-provided ID or command result.
57. Contract and Side Effects
Method should state whether it writes audit/outbox.
caseRepository.save(caseFile); // does it append outbox?
Ambiguous.
Better:
- repository only saves aggregate;
- application appends audit/outbox;
- or method name says:
saveAndAppendDomainEvents(...)
Be explicit.
58. Contract and Layering
DAO contract can mention SQL/table.
Repository contract should mention aggregate/domain.
Query service contract should mention projection/freshness.
Do not expose SQL details in domain repository interface unless needed.
59. Contract Review Checklist
- Method name communicates intent.
- Cardinality is clear.
- Nullability is clear.
- Return type does not hide outcomes.
- Collection result is bounded or documented.
- Pagination/cursor semantics clear.
- Tenant/scope explicit.
- Transaction expectation documented.
- Locking visible in name/contract.
- Consistency/freshness documented.
- Update count handled.
- Error semantics documented.
- Idempotency/retry behavior clear.
- Side effects audit/outbox clear.
- No raw stringly-typed domain values.
- Boolean flags avoided.
- Tests cover contract edge cases.
60. Anti-Pattern: null Means Everything
return null;
for not found, unauthorized, error, or no result.
Use Optional, result type, or exception.
61. Anti-Pattern: boolean save(...)
False is ambiguous.
Use exceptions/result.
62. Anti-Pattern: findAll
Unbounded and usually dangerous.
Use page/chunk/query.
63. Anti-Pattern: Raw Stream Without Close Contract
Leaks connection.
Use try-with-resources, callback, or chunk.
64. Anti-Pattern: Hidden Lock Flag
findById(id, true)
Use findByIdForUpdate.
65. Anti-Pattern: Repository Method That Sometimes Inserts, Sometimes Updates
Unless named upsert, this hides conflict.
66. Anti-Pattern: Method Name Hides Eventual Consistency
getCaseStatus(id)
but reads stale read model.
Name/doc freshness.
67. Mini Lab
Redesign these contracts:
User getUser(String id);
boolean updateStatus(UUID id, String status);
List<Case> findCases(Map<String, Object> filters);
Stream<Row> export(String type);
void save(Object entity);
List<Assignment> findAssignments(UUID caseId, boolean activeOnly, boolean lock);
For each:
- What is ambiguous?
- What should return type be?
- What value objects are needed?
- Is result bounded?
- Is transaction needed?
- Does it lock?
- What errors are expected?
- What name is clearer?
68. Example Redesign
Before:
boolean updateStatus(UUID id, String status);
After:
void approveWithExpectedVersion(
Connection connection,
TenantId tenantId,
CaseFileId caseId,
ExpectedVersion expectedVersion,
Instant approvedAt
);
or use case:
ApproveCaseResult handle(ApproveCaseCommand command);
Now:
- operation is domain-specific;
- tenant scoped;
- versioned;
- timestamp explicit;
- failure via semantic exception;
- transaction participation explicit.
69. Summary
Data access contract design is a major correctness lever.
You must master:
- naming semantics;
findvsgetvsloadFor;- explicit insert/update/upsert;
- Optional vs exception;
- empty collection;
- page vs slice vs cursor;
- stream resource lifecycle;
- result object vs boolean;
- tenant/scope;
- transaction expectation;
- locking in method name;
- consistency/freshness contract;
- idempotency;
- update count;
- semantic errors;
- value objects;
- avoiding boolean flags and raw maps;
- code review checklist.
This closes Phase 4. Part berikutnya masuk Phase 5: JPA dan Hibernate Production Patterns, dimulai dari entity modeling untuk persistence, bukan sekadar class design.
70. References
- Oracle Java SE
Optional: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Optional.html - Oracle Java SE
Stream: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/Stream.html - Oracle Java SE JDBC
Connection: https://docs.oracle.com/en/java/javase/21/docs/api/java.sql/java/sql/Connection.html - Oracle Java SE JDBC
PreparedStatement: https://docs.oracle.com/en/java/javase/21/docs/api/java.sql/java/sql/PreparedStatement.html - Jakarta Persistence Specification: https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2
- Spring Data Commons Repositories: https://docs.spring.io/spring-data/commons/reference/repositories.html
You just completed lesson 32 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.