Build CoreOrdered learning track

Idempotent Command Handling di API Layer

Learn AWS Application and Database - Part 020

Idempotent command handling di API layer AWS: Idempotency-Key, command store, request hash, transaction boundary, response replay, dan safe retry untuk mutation endpoints.

8 min read1455 words
PrevNext
Lesson 2096 lesson track18–52 Build Core
#aws#api-gateway#idempotency#command-handling+6 more

Part 020 — Idempotent Command Handling di API Layer

Tujuan bagian ini: membangun mutation endpoint yang aman terhadap retry, timeout, duplicate submit, network failure, dan ambiguous commit. Fokusnya bukan hanya Idempotency-Key, tetapi keseluruhan command handling state machine dari API sampai database dan side effect.

Command endpoint seperti ini berbahaya bila tidak idempotent:

POST /orders
POST /payments
POST /case-escalations
POST /refunds
PATCH /cases/{id}/status
POST /documents/{id}/approve

Mengapa?

Karena client bisa mengirim request lebih dari sekali walaupun user hanya klik sekali. Penyebabnya:

- mobile network timeout,
- browser retry,
- load balancer connection reset,
- API Gateway 504,
- backend response hilang setelah commit,
- client tidak menerima response,
- user double click,
- worker retry,
- deployment restart,
- SDK retry,
- upstream event replay.

Tanpa idempotent command handling, duplicate request bisa menjadi duplicate effect:

- order ganda,
- payment ganda,
- email ganda,
- status transition ganda,
- inventory berkurang dua kali,
- enforcement action tereksekusi dua kali,
- audit trail misleading,
- case lifecycle melompat state.

Mental model:

Idempotency bukan fitur HTTP.
Idempotency adalah invariant bisnis yang disimpan secara durable.

1. Apa Itu Command?

Dalam seri ini:

Command = permintaan untuk mengubah state.

Contoh command:

CreateOrder,
ReserveInventory,
SubmitCase,
ApproveDocument,
EscalateInvestigation,
CreatePayment,
IssueRefund,
AssignInspector,
CloseEnforcementCase.

Command punya karakteristik:

- memiliki intent,
- bisa diterima atau ditolak,
- bisa mengubah state,
- bisa memicu side effect,
- butuh authorization,
- butuh validation,
- butuh audit,
- sering butuh idempotency,
- sering punya lifecycle.

Command bukan sekadar HTTP request body. Command adalah unit semantik.


2. Idempotency: Definisi yang Berguna di Production

Definisi formal sederhana:

Operasi idempotent menghasilkan efek akhir yang sama walaupun dijalankan lebih dari sekali dengan input yang sama.

Namun untuk API command, definisi yang lebih operasional:

Untuk identity command yang sama dan payload semantik yang sama, sistem harus melakukan side effect paling banyak sekali, dan duplicate attempt harus mengembalikan hasil yang konsisten.

Ada tiga bagian:

1. Same command identity.
2. Same semantic request.
3. Same durable outcome atau same observable status.

Jika hanya memakai Idempotency-Key tetapi tidak memverifikasi payload, bug tetap bisa terjadi.

Contoh buruk:

Request 1: Idempotency-Key: abc, amount=100
Request 2: Idempotency-Key: abc, amount=999

Sistem tidak boleh diam-diam menganggap request kedua sama. Response yang benar biasanya 409 Conflict atau 422/400 tergantung contract.


3. Idempotency-Key Bukan Command ID Selalu

Ada beberapa identifier berbeda.

IdentifierDibuat olehFungsi
Request-Idgateway/systemIdentitas attempt teknis. Berubah tiap attempt.
Correlation-Idclient/gatewayMenghubungkan trace lintas service. Bisa sama untuk satu user journey.
Idempotency-Keyclient/callerIdentitas logical command dari sisi caller. Stabil saat retry.
Command-Idserver atau derivedIdentitas durable command di sistem.
Aggregate-IddomainEntity yang diubah command.
Event-Idevent publisherIdentitas fact yang diterbitkan setelah state berubah.

Jangan mencampur semuanya.

Salah:

Gunakan requestId sebagai idempotency key.

Mengapa salah? Karena request id biasanya berbeda pada retry.

Lebih benar:

Idempotency-Key stabil untuk logical command.
Request-Id berbeda untuk tiap technical attempt.
Correlation-Id mengikat semua attempt dalam user journey.

4. State Machine Command Handling

Idempotent command handling sebaiknya dimodelkan sebagai state machine.

Minimal statuses:

PROCESSING,
COMPLETED,
FAILED_RETRYABLE,
FAILED_FINAL,
AMBIGUOUS.

Untuk sistem sederhana, bisa mulai dari:

IN_PROGRESS,
COMPLETED,
FAILED.

Tetapi untuk payment, case escalation, atau regulatory workflow, bedakan failure final dan ambiguous.


5. Basic Algorithm

Algorithm umum:

1. Receive command request.
2. Authenticate and authorize caller.
3. Validate request shape.
4. Canonicalize semantic payload.
5. Compute request hash.
6. Try reserve idempotency record atomically.
7. If record already exists:
   a. If hash differs -> conflict.
   b. If completed -> return stored response/status.
   c. If processing -> return 202/409 depending contract.
   d. If failed retryable -> retry or resume safely.
   e. If ambiguous -> return status requiring reconciliation.
8. Execute domain transaction.
9. Store result and response summary.
10. Publish outbox/event or start workflow safely.
11. Return response.

Diagram:


6. Validation Order: Shape, Auth, Semantics, Idempotency

Urutan matters.

Recommended order:

1. Parse request.
2. Authenticate caller.
3. Authorize operation.
4. Validate shape and required fields.
5. Canonicalize semantic payload.
6. Build idempotency scope.
7. Reserve/check idempotency record.
8. Execute domain validation inside transaction.
9. Commit command result.

Mengapa auth sebelum idempotency?

Karena idempotency key tidak boleh menjadi oracle untuk mengecek existence command milik caller lain.

Mengapa shape validation sebelum idempotency?

Karena request invalid tidak perlu mencemari idempotency store.

Mengapa domain validation sering setelah reserve?

Karena beberapa domain validation terkait state saat command diterima dan perlu hasilnya konsisten pada retry.

Contoh:

Approve case saat status = REVIEWED.
Retry command approval harus mengembalikan hasil command pertama,
bukan gagal karena status sekarang sudah APPROVED.

7. Idempotency Scope

Key harus discoped.

Jangan hanya:

idempotency_key = abc

Gunakan scope:

tenant_id + caller_id/client_id + operation + idempotency_key

Contoh unique key:

UNIQUE (tenant_id, caller_id, operation, idempotency_key)

Mengapa?

Karena dua tenant berbeda bisa memakai idempotency key yang sama. Dua operation berbeda juga bisa kebetulan memakai key sama.

Scope yang salah bisa menyebabkan:

- false duplicate,
- cross-tenant leakage,
- denial of service,
- security issue,
- impossible debugging.

8. Request Hash dan Canonicalization

Request hash digunakan untuk memastikan key yang sama tidak dipakai untuk payload berbeda.

Jangan hash raw JSON string langsung jika field order tidak stabil.

Buruk:

{"amount":100,"currency":"USD"}

vs

{"currency":"USD","amount":100}

Secara semantic sama, raw string berbeda.

Gunakan canonicalization:

- normalize field order,
- remove ignored fields,
- normalize numeric precision,
- normalize timestamps bila contract mengizinkan,
- include operation name,
- include caller/tenant scope,
- include version bila schema berubah.

Pseudocode:

String canonical = canonicalJson(Map.of(
    "operation", "CreatePayment",
    "tenantId", tenantId,
    "callerId", callerId,
    "amount", money.amountMinorUnits(),
    "currency", money.currency(),
    "orderId", orderId
));

String requestHash = sha256(canonical);

Jangan masukkan field volatile:

requestId,
traceId,
signature timestamp,
client local timestamp,
headers yang tidak memengaruhi semantics.

9. SQL/Aurora/RDS Command Store Pattern

Schema contoh:

CREATE TABLE api_command_idempotency (
    id BIGSERIAL PRIMARY KEY,
    tenant_id VARCHAR(64) NOT NULL,
    caller_id VARCHAR(128) NOT NULL,
    operation VARCHAR(128) NOT NULL,
    idempotency_key VARCHAR(256) NOT NULL,
    request_hash CHAR(64) NOT NULL,
    command_id UUID NOT NULL,
    aggregate_type VARCHAR(128),
    aggregate_id VARCHAR(128),
    status VARCHAR(32) NOT NULL,
    http_status INT,
    response_body JSONB,
    error_code VARCHAR(128),
    locked_until TIMESTAMPTZ,
    expires_at TIMESTAMPTZ NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE (tenant_id, caller_id, operation, idempotency_key)
);

CREATE INDEX idx_api_command_expiry
    ON api_command_idempotency (expires_at);

CREATE INDEX idx_api_command_status
    ON api_command_idempotency (status, updated_at);

Reserve command atomically:

INSERT INTO api_command_idempotency (
    tenant_id,
    caller_id,
    operation,
    idempotency_key,
    request_hash,
    command_id,
    status,
    expires_at
)
VALUES (?, ?, ?, ?, ?, ?, 'PROCESSING', now() + interval '24 hours')
ON CONFLICT (tenant_id, caller_id, operation, idempotency_key)
DO NOTHING;

Jika insert berhasil, attempt ini owner baru. Jika tidak, baca existing record.

Important:

Unique constraint adalah lock-free correctness primitive.
Jangan implementasikan idempotency dengan read-then-insert tanpa unique constraint.

Read-then-insert race:


10. Domain Transaction dengan Command Record

Ada dua pilihan umum.

Option A — Command Record dan Domain Write dalam Satu Transaction

Cocok bila idempotency store dan domain data berada di database yang sama.

BEGIN;

-- reserve idempotency record or lock existing
-- validate current aggregate state
-- update domain tables
-- write outbox event
-- update idempotency record COMPLETED + response summary

COMMIT;

Keuntungan:

- atomic,
- sederhana untuk relational DB,
- outbox bisa satu transaction,
- failure semantics lebih jelas.

Kekurangan:

- transaksi bisa menjadi besar bila response body besar,
- tidak cocok bila side effect external dilakukan di tengah transaction,
- contention jika command store terlalu hot.

Option B — Reserve Record, Lalu Domain Transaction Terpisah

Cocok bila command store berada di DynamoDB atau operasi memulai workflow.

1. Reserve idempotency record.
2. Execute domain transaction.
3. Update idempotency record with outcome.

Kelemahan:

Ada gap antara domain commit dan idempotency completion update.

Perlu reconciliation.


11. DynamoDB Command Store Pattern

DynamoDB cocok untuk idempotency store karena conditional write kuat untuk reserve key.

Key design:

PK = TENANT#{tenantId}#CALLER#{callerId}#OP#{operation}
SK = IDEMPOTENCY#{idempotencyKey}

Attributes:

requestHash,
commandId,
status,
httpStatus,
responseSummary,
errorCode,
expiresAt,
createdAt,
updatedAt,
lockExpiresAt.

Reserve dengan condition:

{
  "TableName": "ApiCommandIdempotency",
  "Item": {
    "pk": {"S": "TENANT#t1#CALLER#c1#OP#CreatePayment"},
    "sk": {"S": "IDEMPOTENCY#abc"},
    "requestHash": {"S": "..."},
    "commandId": {"S": "cmd-123"},
    "status": {"S": "PROCESSING"},
    "expiresAt": {"N": "1780000000"}
  },
  "ConditionExpression": "attribute_not_exists(pk) AND attribute_not_exists(sk)"
}

Jika ConditionalCheckFailedException, baca item existing.

DynamoDB conditional writes dapat menjadi primitive idempotency karena write dilakukan hanya jika kondisi yang diharapkan masih benar.


12. In-Progress Duplicate Handling

Saat duplicate attempt datang sementara command pertama masih PROCESSING, ada beberapa response strategy.

StrategyResponseCocok untuk
Return 202 Accepted + status URLCaller pollingLong-running command
Return 409 Conflict with COMMAND_IN_PROGRESSClient harus menungguCommand yang tidak boleh concurrent
Wait briefly then replay if completedUX synchronousCommand sangat cepat dan duplicate immediate
Attach to same workflow statusWorkflow commandStep Functions / job pattern

Jangan menjalankan domain transaction kedua.

Response contoh:

{
  "status": "PROCESSING",
  "commandId": "cmd-123",
  "statusUrl": "/commands/cmd-123",
  "retryable": true,
  "retryAfterSeconds": 2
}

13. Completed Duplicate Handling

Jika status COMPLETED, duplicate attempt harus mengembalikan hasil yang konsisten.

Ada dua model.

Replay Full Response

Simpan response body dan status code.

Duplicate -> return exactly same HTTP status and response body.

Cocok untuk:

- create payment,
- create order,
- create resource,
- command yang response-nya kecil.

Return Current Resource Representation

Duplicate return current state dari resource.

Cocok bila:

- resource state bisa berubah setelah command,
- response original tidak perlu identik,
- contract menyatakan response adalah current representation.

Lebih aman untuk idempotency adalah replay response summary minimal:

{
  "commandId": "cmd-123",
  "resourceId": "payment-789",
  "status": "SUCCEEDED"
}

Jangan simpan response besar atau sensitif tanpa redaction/encryption policy.


14. Failed Duplicate Handling

Failure tidak selalu sama.

Failure typeRetry behaviorExample
Validation finalJangan retryAmount invalid, forbidden transition
Authorization finalJangan retryCaller tidak boleh operasi
Conflict finalCaller action requiredVersion mismatch
Transient infraRetry possibleDB timeout before transaction
AmbiguousCheck/reconcileProvider timeout after side effect call

Jangan semua FAILED diperlakukan sama.

Untuk command kritikal, gunakan:

FAILED_FINAL,
FAILED_RETRYABLE,
AMBIGUOUS.

15. External Side Effect: Boundary Paling Berbahaya

Database transaction bisa rollback. External side effect tidak selalu bisa rollback.

Contoh:

charge payment,
send email,
submit government filing,
create third-party ticket,
publish irreversible notification,
call vendor that lacks idempotency.

Pattern aman:

1. Durable command record.
2. Durable side-effect attempt record.
3. Provider idempotency key bila tersedia.
4. Outbox/workflow untuk eksekusi side effect.
5. Reconciliation untuk ambiguous result.

Diagram:

Jangan panggil provider irreversible di tengah transaksi database panjang.


16. Idempotency dengan Step Functions

Jika command memulai workflow:

POST /case-escalations
-> create command record
-> StartExecution Step Functions
-> return command/workflow status

Untuk Standard Workflows, StartExecution idempotent bila name dan input sama saat execution masih berjalan. Jika execution sudah closed atau input berbeda, API mengembalikan error ExecutionAlreadyExists, dan nama bisa dipakai ulang setelah 90 hari menurut dokumentasi AWS.

Tetapi jangan mengandalkan ini sebagai satu-satunya idempotency API layer.

Mengapa?

- Express Workflows tidak punya idempotency StartExecution yang sama.
- Command response contract tetap perlu disimpan.
- Caller butuh status endpoint.
- Execution name uniqueness bukan pengganti domain invariant.
- Input canonicalization tetap perlu.

Pattern:

executionName = safeSlug(commandId or idempotencyScopeHash)

Simpan mapping:

commandId -> stepFunctionsExecutionArn

17. API Gateway, Lambda Powertools, dan Idempotency

AWS Lambda Powertools menyediakan utility idempotency untuk membuat Lambda handler retry-safe dengan persistence layer seperti DynamoDB.

Itu berguna untuk:

- Lambda-backed API endpoint,
- async consumer,
- event handler,
- simple command handler,
- deterministic response replay.

Tetapi tetap pahami boundary:

Powertools membantu implementasi mekanik idempotency.
Ia tidak menggantikan desain command semantics, request hash, domain transaction, external side-effect reconciliation, dan status contract.

Gunakan library untuk mengurangi boilerplate. Jangan gunakan library untuk menghindari reasoning.


18. Optimistic Concurrency dan Idempotency Berbeda

Idempotency menjawab:

Apakah command yang sama dijalankan lebih dari sekali?

Optimistic concurrency menjawab:

Apakah command ini masih valid terhadap versi aggregate saat ini?

Contoh:

PATCH /cases/case-123/status
Idempotency-Key: k-1
If-Match: "version-7"

Kemungkinan:

- First attempt valid, update version 7 -> 8.
- Retry dengan key sama harus return original success.
- New command dengan key berbeda tapi If-Match version 7 harus gagal 412.

Tanpa idempotency, retry bisa gagal palsu karena version sudah berubah oleh attempt pertama. Tanpa optimistic concurrency, command berbeda bisa overwrite state tanpa sadar.

Keduanya dibutuhkan.


19. Idempotency TTL

Idempotency record tidak harus disimpan selamanya untuk semua operasi.

Tetapi TTL harus sesuai risiko bisnis.

CommandSuggested TTL thinking
Create UI draftpendek, misal jam/hari
Create orderhari/minggu sesuai retry dan support window
Payment/refundpanjang sesuai audit/chargeback/reconciliation
Regulatory enforcement actionsangat panjang atau permanen sebagai audit record
Export reportsampai job/result expired

Jangan pilih TTL hanya karena biaya storage.

Pertanyaan desain:

Berapa lama caller boleh retry?
Berapa lama duplicate request masih mungkin datang?
Berapa lama audit membutuhkan bukti?
Apakah command punya external irreversible side effect?
Apa konsekuensi jika key expired lalu request lama datang lagi?

Jika key expired, duplicate lama bisa menjadi command baru. Untuk side effect kritikal, TTL pendek bisa berbahaya.


20. Security Considerations

Idempotency store bisa membocorkan informasi bila salah desain.

Risiko:

- caller menebak idempotency key milik caller lain,
- response replay mengembalikan data milik tenant lain,
- status endpoint expose command existence,
- idempotency key disimpan mentah dan muncul di log,
- response body sensitif disimpan tanpa redaction,
- key tidak discoped by tenant/caller/operation.

Mitigasi:

- scope key dengan tenant/caller/operation,
- authorize sebelum lookup,
- hash idempotency key di logs,
- encrypt sensitive response if stored,
- store response summary, not full sensitive payload,
- avoid user-provided key as primary public identifier,
- use status endpoint authorization.

21. Observability

Metric:

idempotency_reserve_success_count,
idempotency_duplicate_in_progress_count,
idempotency_duplicate_completed_count,
idempotency_conflict_count,
idempotency_ambiguous_count,
command_processing_duration_ms,
command_completion_count,
command_failed_final_count,
command_failed_retryable_count,
command_reconciliation_count,
external_side_effect_duplicate_prevented_count.

Log fields:

requestId,
correlationId,
tenantId bucket/hash,
operation,
idempotencyKeyHash,
requestHash,
commandId,
aggregateId,
statusBefore,
statusAfter,
httpStatus,
retryAttempt,
sideEffectProvider,
providerIdempotencyKeyHash,
errorCode.

Trace:

API receive -> idempotency reserve -> domain transaction -> outbox/workflow -> side effect -> command complete

Audit trail:

who requested,
when,
what operation,
which aggregate,
which previous state,
which resulting state,
which idempotency scope,
which side effects,
which reconciliation outcome.

22. Testing Matrix

Test idempotency with concurrency, not just sequential retry.

[ ] Same key + same payload sequential retry returns same result.
[ ] Same key + different payload returns conflict.
[ ] 100 concurrent same-key attempts produce one domain effect.
[ ] Timeout after domain commit returns success/status on retry.
[ ] Timeout before domain commit retries safely.
[ ] External provider timeout enters AMBIGUOUS.
[ ] Reconciliation resolves AMBIGUOUS.
[ ] Expired key behavior matches documented contract.
[ ] Unauthorized caller cannot discover another caller's command.
[ ] Replay response does not expose sensitive data.
[ ] Duplicate in-progress returns 202/409 as contract states.
[ ] Optimistic concurrency retry with same key succeeds/replays.
[ ] New key with stale version fails 412.

Concurrency test pseudocode:

int attempts = 100;
ExecutorService pool = Executors.newFixedThreadPool(20);
List<Future<Response>> futures = new ArrayList<>();

for (int i = 0; i < attempts; i++) {
    futures.add(pool.submit(() -> callCreatePayment(
        "Idempotency-Key", "same-key-123",
        Map.of("amount", 1000, "currency", "USD")
    )));
}

List<Response> responses = futures.stream().map(Future::get).toList();

assertThat(countPaymentRowsForCommand("same-key-123")).isEqualTo(1);
assertThat(countProviderCharges("same-key-123")).isEqualTo(1);
assertThat(responses).allMatch(r -> r.isSuccess() || r.isAcceptedInProgress());

23. Example: Create Payment Command

Request:

POST /payments
Idempotency-Key: pay-20260706-0001
Content-Type: application/json
{
  "orderId": "ord-123",
  "amount": 125000,
  "currency": "IDR",
  "paymentMethodId": "pm-456"
}

Flow:

1. API validates caller can pay order ord-123.
2. Build idempotency scope tenant+caller+CreatePayment+key.
3. Compute request hash.
4. Reserve command record.
5. Check order payable.
6. Mark payment as PENDING.
7. Write outbox PaymentChargeRequested.
8. Return 202 or 201 depending synchronous/asynchronous contract.
9. Worker calls provider using provider idempotency key.
10. Reconciliation handles provider timeout.

Key point:

The API command completion and provider charge completion do not have to be the same synchronous boundary.

24. Example: Case Status Transition

Request:

PATCH /cases/case-123/status
Idempotency-Key: esc-abc
If-Match: "case-version-17"
{
  "targetStatus": "ESCALATED",
  "reasonCode": "HIGH_RISK_FINDING"
}

Flow:

1. Authorize case transition.
2. Reserve command key.
3. Check request hash.
4. In transaction:
   - load case with version 17,
   - validate transition REVIEWED -> ESCALATED,
   - update status,
   - increment version,
   - write audit event,
   - write outbox CaseEscalated.
5. Mark command completed.
6. Return status.

Retry behavior:

Same key + same payload -> return original success.
Different key + stale If-Match -> 412.
Same key + different targetStatus -> 409 idempotency conflict.

This distinction is critical in regulatory workflow systems.


25. Anti-Pattern

Anti-Pattern 1 — Idempotency Key Without Request Hash

Same key can accidentally apply to different request. This is dangerous.

Anti-Pattern 2 — Read-Then-Insert Without Unique Constraint

Concurrent attempts can both pass the read and both insert effects.

Anti-Pattern 3 — Store Only “Seen Key”

A boolean seen flag is not enough. You need status, request hash, outcome, timestamps, and failure classification.

Anti-Pattern 4 — Retry Non-Idempotent External Side Effect

Calling payment provider twice without provider idempotency key is how duplicate charges happen.

Anti-Pattern 5 — Treat 504 as Failed Command

504 is unknown, not failed.

Anti-Pattern 6 — TTL Too Short for Critical Commands

Expired idempotency record can allow duplicate old command.

Anti-Pattern 7 — No Status Endpoint

If command can outlive request, status endpoint is part of the contract.


26. Production Checklist

[ ] Every mutation endpoint has command semantics documented.
[ ] Idempotency-Key required for non-trivial command.
[ ] Idempotency scope includes tenant/caller/operation.
[ ] Request hash uses canonical semantic payload.
[ ] Same key + different hash returns conflict.
[ ] Reserve operation is atomic via unique constraint/conditional write.
[ ] Domain transaction cannot double-apply command.
[ ] Duplicate in-progress response is defined.
[ ] Duplicate completed response is defined.
[ ] Failure statuses distinguish final, retryable, and ambiguous.
[ ] External side effect has provider idempotency or side-effect record.
[ ] Timeout after commit is handled by status/replay.
[ ] TTL matches business risk.
[ ] Idempotency key is not leaked in logs unmasked.
[ ] Response replay does not expose unauthorized/sensitive data.
[ ] Metrics distinguish new, duplicate, conflict, in-progress, ambiguous.
[ ] Concurrency tests prove one side effect under 100 duplicate attempts.

27. Mental Model Ringkas

API request adalah attempt.
Command adalah logical intent.
Idempotency key menghubungkan attempts ke command yang sama.
Request hash memastikan command yang sama tidak berubah makna.
Command store menyimpan lifecycle.
Database constraint/conditional write memberi atomicity.
Outbox/workflow menjaga side effect tidak hilang.
Status endpoint mengatasi unknown outcome.
Reconciliation menyelesaikan ambiguity.

Idempotency bukan “return same response kalau key sama”. Itu hanya permukaan.

Idempotency production adalah kemampuan sistem untuk berkata:

Saya tahu command ini sudah pernah diterima,
saya tahu apakah payload-nya sama,
saya tahu apakah sedang diproses atau sudah selesai,
saya tahu side effect apa yang sudah terjadi,
dan saya bisa memberi caller jawaban yang aman tanpa menjalankan efek ganda.

28. Referensi

Lesson Recap

You just completed lesson 20 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.