Build CoreOrdered learning track

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.

12 min read2306 words
PrevNext
Lesson 3260 lesson track12–33 Build Core
#java#data-access#api-design#repository+6 more

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 getUser return null?

Apakah list bounded?

Apakah save insert atau update?

Apakah false berarti 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:

PrefixSuggested 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>>;
  • Optional field in JPA entity;
  • returning null instead of Optional.empty;
  • Optional parameter 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 Optional for optional single;
  • use empty collection;
  • validate constructor arguments;
  • use @Nullable only 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:

  1. What is ambiguous?
  2. What should return type be?
  3. What value objects are needed?
  4. Is result bounded?
  5. Is transaction needed?
  6. Does it lock?
  7. What errors are expected?
  8. 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;
  • find vs get vs loadFor;
  • 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

Lesson Recap

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.

Continue The Track

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