P0 / P1 · Chapter 05 / 15
Kafka / Async / Database
The 60-second cheat sheet#
An HTTP response can acknowledge work before an event-driven system finishes it. In the illustrative verification flow: request → producer → Kafka broker → consumer → database projection → status API → UI. A 202 response proves acceptance under the contract; a final business state and its correlated effects prove completion.
Delivery, ordering and consistency#
| Concept | What to test / avoid claiming |
|---|---|
| At-least-once delivery | Duplicate processing is possible; consumers need business deduplication |
| Ordering | Usually scoped to partition/key and producer/consumer configuration, not global |
| Retry | Distinguish transient processing failure from permanently invalid input |
| Dead-letter queue | Verify routing, diagnostic reason and recovery policy through approved hooks |
| Out-of-order events | Version/sequence rule prevents older state overwriting newer state |
| Eventual consistency | Observable delay is expected; bound convergence by agreed service objective |
| Transaction boundary | DB commit, event publication and projection visibility may be separate |
Kafka exactly-once features have a defined transactional boundary and do not automatically make arbitrary database/external side effects exactly once. Test idempotent handlers with the same business/event ID and verify one logical effect, including after consumer restart.
Bounded polling for completion#
// tests/api/verification.spec.ts: complete module, illustrative sandbox contract.
import { test, expect } from '@playwright/test';
test('accepted verification reaches final state', async ({ request }) => {
// Fixture-provided unique user assumed; no production accounts.
const id = process.env.QA_VERIFICATION_USER_ID;
if (!id) throw new Error('Unique sandbox user required');
const path = `/api/users/${encodeURIComponent(id)}/verify`;
const accepted = await request.post(path);
expect(accepted.status()).toBe(202);
let observed = 'UNOBSERVED';
await expect.poll(async () => {
const response = await request.get(`/api/users/${encodeURIComponent(id)}/verification`);
expect(response.status()).toBe(200);
const body: unknown = await response.json();
if (!body || typeof body !== 'object' || !('status' in body) || typeof body.status !== 'string') {
throw new Error('Invalid status response');
}
observed = body.status;
return ['VERIFIED', 'REJECTED'].includes(observed);
}, { timeout: 20_000, intervals: [200, 500, 1_000], message: 'verification terminal state' }).toBe(true);
expect(observed).toBe('VERIFIED'); // REJECTED is terminal but fails immediately here
});
This poll stops on either known terminal outcome, then asserts success. Exceptions inside poll may be retried; a malformed/unavailable response is reported by the final poll failure and should include diagnostics. If unknown states must abort immediately, use a dedicated bounded polling helper with explicit error classification. Set test budget above the poll plus setup/teardown. Do not repeat the initiating POST in the polling callback.
End-to-end evidence plan#
Advanced: stop immediately on auth/schema/terminal failures
expect.poll is convenient, but retrying every exception can conceal a permanent 401 or schema defect until the deadline. The helper below retries only documented transient reads/PENDING, fails immediately on rejected/invalid states and bounds elapsed time. It depends on an injected Playwright APIRequestContext and a sandbox endpoint. A response timeout after starting verification still requires reconciliation before replaying the mutation.
import type { APIRequestContext } from '@playwright/test';
import { performance } from 'node:perf_hooks';
export async function waitVerified(api: APIRequestContext, path: string, timeoutMs = 15_000): Promise<void> {
const deadline = performance.now() + timeoutMs;
while (performance.now() < deadline) {
const remaining = Math.max(1, Math.floor(deadline - performance.now()));
const response = await api.get(path, { timeout: remaining });
const code = response.status();
if (code !== 200 && code !== 503) throw new Error(`Fatal status read: HTTP ${code}`);
if (code === 200) {
const body: unknown = await response.json();
if (!body || typeof body !== 'object' || !('status' in body)) throw new Error('Invalid status schema');
if (body.status === 'VERIFIED') return;
if (body.status !== 'PENDING') throw new Error(`Terminal/unknown state: ${String(body.status)}`);
}
const delay = Math.min(500, Math.max(0, deadline - performance.now()));
await new Promise<void>(resolve => setTimeout(resolve, delay));
}
throw new Error('Verification completion deadline exceeded');
}
The short delay is a polling interval between meaningful reads, not a fixed assertion substitute. Here 503 retry is an explicit hypothetical contract policy; transport exceptions fail immediately. Extend only with classified transient transport handling and cancellation if the real environment requires it. Request timeouts bound calls, not every conceivable body-parsing or process-scheduling delay.
- Provision an authorized unique sandbox user and correlation ID; stub external identity/payment side effects.
- Submit verification and assert 202 plus accepted operation ID if documented.
- Through public status API poll to the contract's terminal state; verify the UI reflects that same user's result.
- With authorized hooks, inspect Kafka key/event ID/schema and consumer/projection logs; check DB row and audit record for the same correlation.
- Test duplicate delivery, delayed consumption, rejection, consumer recovery and dead-letter handling separately.
- Clean records or use documented TTL; retain sanitized failure evidence.
Public API completion proves externally observable behavior, not exactly which internal consumer ran. Kafka UI or DB access requires approved test harness permissions; do not pretend a browser test automatically observes internal events. A UI heading alone proves neither accepted event nor persisted result.
Reconciliation and ETL validation#
| Check | Concrete method |
|---|---|
| Completeness | Expected source IDs minus target IDs; count alone can hide substitutions |
| Duplicates | Group by business/event ID and flag count > 1 |
| Accuracy | Compare amount/currency/status with explicit mapping and decimal rules |
| Integrity | Foreign keys, orphan detection, mandatory fields, state consistency |
| Freshness | Compare UTC event/ingestion timestamps against agreed window |
| Totals | Reconcile by currency/time window; never sum unrelated currencies |
| Lineage | Source key + correlation + transformation version + target identity |
After-write reads can race with transaction commit or read replicas. Retry only where eventual consistency is expected; retrying an invariant on a strongly consistent endpoint may hide a bug. For multi-query reconciliation, choose a stable snapshot/watermark or allow a documented lag window. Align time zones and late-arriving events before classifying missing data. FINA ETL/data integrity experience provides a grounded story; add only true ownership/results.
Observability and six scenarios#
| Scenario | Short ideal answer | Detailed diagnostic route |
|---|---|---|
| 202 but UI stays PENDING | Acceptance is not completion | Follow operation ID through producer, broker, consumer lag, DB projection and status API; poll within objective |
| Duplicate verified event | Assert one logical effect | Check dedupe key persistence across restarts, audit/ledger duplicates and processing transaction |
| Old event resets status | Define ordering/version contract | Use same key, controlled out-of-order versions, verify older version ignored or handled explicitly |
| DB correct, API stale | Check read path/replica/cache | Compare commit time, replica lag, cache invalidation, correlation and declared consistency |
| Record counts match but totals differ | Counts insufficient | Reconcile keys, decimal/currency mapping, duplicates and missing paired rows |
| Consumer fails repeatedly | Bound retries and DLQ policy | Inject safe controlled failure, inspect retry count/reason, DLQ ownership and authorized replay |
Interview response#
30 seconds: “I distinguish transport acceptance from final business state. I correlate the request, event, projection and API result, poll a meaningful terminal state with a bounded deadline, and test duplicates and ordering according to the actual Kafka contract. For reconciliation I compare keys and values as well as counts.”
90-second senior answer: async verification investigation
I first confirm whether the endpoint promises synchronous completion or only acceptance. For 202 I record operation/correlation identity, then poll the supported status endpoint rather than sleep. On timeout I inspect where evidence stops: event publication, broker partition, consumer processing, database commit, read model or UI refresh. I distinguish consumer lag from a permanent rejection and verify error/DLQ handling. Duplicate events should not create duplicate business effects; out-of-order behavior depends on keys and version rules. A DB check needs the correct consistency boundary and a safe read account. I report which layer is proven by each observation, protect external financial side effects, and retain enough evidence to make the failure reproducible.
Memory hook: accepted is a beginning; correlate, converge, reconcile.
Quick practice · reveal answers#
What does HTTP 202 prove?
- Acceptance according to contract, not final outcome.
- Poll the operation/status endpoint to a terminal business state.
Pitfall: Equating HTTP code with VERIFIED status field.
How do you test Kafka duplicates?
- Send controlled duplicate business/event identity and verify one logical DB/audit/ledger effect.
- Include restart boundary when relevant.
Pitfall: Assuming delivery semantics guarantee all external effects once.
Is Kafka globally ordered?
- Ordering depends on partition/key and configuration.
- Specify the exact ordering/version contract and test out-of-order handling.
Pitfall: Expecting global order across unrelated partitions.
How do you reconcile source and target?
- Compare keys both directions
- duplicates, values and compatible grouped totals with a stable watermark/snapshot.
Pitfall: Equal counts imply identical records.
Official references and related topics#
Optional online further reading; all core study material is included above.