Design IndexedDB pending-operation persistence
Choose fields, expiry, schema versioning, conflict handling, and cleanup rules for pending transfer records.
Answer Strategy
Treat indexedDB pending-operation persistence as a product surface with user workflows, failure modes, and operational constraints. Start by clarifying who uses it, what must be fast, what must be shareable, what permissions apply, and what data can be stale.
A strong senior answer separates five owners before naming components: URL state for shareable intent, server state for canonical data, local interaction state for in-progress UI, design-system primitives for reusable behavior, and telemetry for production proof.
Then turn the whiteboard into implementation contracts. The code below is a compact architecture registry: it forces you to name routes, state owners, loading/error states, accessibility obligations, performance budgets, and rollout checks.
Reference Implementation: Design IndexedDB pending-operation persistence Architecture Contract
Use this as the implementation anchor during a frontend system design interview. It is deliberately framework-light so you can adapt it to React, Next.js, Remix, or a component platform.
type StateOwner = 'url' | 'server' | 'client' | 'design-system' | 'telemetry';
type RiskLevel = 'low' | 'medium' | 'high';
type FrontendBoundary = {
owner: StateOwner;
owns: string;
examples: string[];
failureMode: string;
};
type SystemDesignPlan = {
surface: string;
primaryWorkflow: string;
boundaries: FrontendBoundary[];
performanceBudget: {
firstUsefulPaintMs: number;
interactionResponseMs: number;
maxInitialClientKb: number;
};
rollout: Array<{ risk: RiskLevel; check: string }>;
};
type RequestState<T> =
| { tag: 'idle' }
| { tag: 'loading'; previous?: T }
| { tag: 'success'; data: T; receivedAt: number }
| { tag: 'error'; message: string; previous?: T };
type ApiResult<T> = { ok: true; data: T } | { ok: false; message: string };
async function getJson<T>(
url: string,
signal?: AbortSignal
): Promise<ApiResult<T>> {
try {
const response = await fetch(url, { signal, headers: { accept: 'application/json' } });
if (!response.ok) return { ok: false, message: 'HTTP ' + response.status };
return { ok: true, data: (await response.json()) as T };
} catch (error) {
if (signal?.aborted) return { ok: false, message: 'Request cancelled' };
return {
ok: false,
message: error instanceof Error ? error.message : 'Unknown error',
};
}
}
function useRemoteResource<T>(url: string) {
const [state, setState] = React.useState<RequestState<T>>({ tag: 'idle' });
React.useEffect(() => {
const controller = new AbortController();
setState((previous) => ({
tag: 'loading',
previous: previous.tag === 'success' ? previous.data : undefined,
}));
getJson<T>(url, controller.signal).then((result) => {
if (controller.signal.aborted) return;
if (result.ok) {
setState({ tag: 'success', data: result.data, receivedAt: Date.now() });
} else {
setState((previous) => ({
tag: 'error',
message: result.message,
previous: previous.tag === 'loading' ? previous.previous : undefined,
}));
}
});
return () => controller.abort();
}, [url]);
return state;
}
function createSystemDesignPlan(surface: string): SystemDesignPlan {
return {
surface,
primaryWorkflow: 'User completes the highest-value task without losing context.',
boundaries: [
{
owner: 'url',
owns: 'shareable filters, selected entity, tab, and return path',
examples: ['query string', 'route segment', 'hash for local-only anchors'],
failureMode: 'refresh or shared links drop the user into a different state',
},
{
owner: 'server',
owns: 'canonical entities, permissions, pagination cursors, freshness',
examples: ['typed API client', 'query cache', 'schema validation'],
failureMode: 'UI renders data the user can no longer access or trust',
},
{
owner: 'client',
owns: 'drafts, focused item, optimistic edits, open panels',
examples: ['component state', 'reducer', 'external store when cross-route'],
failureMode: 'in-progress interaction is overwritten by background updates',
},
{
owner: 'design-system',
owns: 'keyboard behavior, accessible labels, density, empty states',
examples: ['combobox', 'dialog', 'table', 'toast', 'skeleton'],
failureMode: 'teams reimplement behavior inconsistently across screens',
},
{
owner: 'telemetry',
owns: 'latency, errors, abandonment, feature flag health',
examples: ['web vitals', 'client errors', 'workflow checkpoints'],
failureMode: 'rollout looks successful because nobody can see silent failure',
},
],
performanceBudget: {
firstUsefulPaintMs: 1800,
interactionResponseMs: 100,
maxInitialClientKb: 180,
},
rollout: [
{ risk: 'high', check: 'guard with feature flag and kill switch' },
{ risk: 'medium', check: 'ship read-only path before mutation-heavy flows' },
{ risk: 'medium', check: 'alert on client error rate and abandoned workflow' },
{ risk: 'low', check: 'document ownership and follow-up extraction points' },
],
};
}Testing Strategy
Convert the answer into observable behavior. In a mid-senior interview, say which behaviors are covered by unit tests, interaction tests, accessibility checks, and one browser smoke path.
test('frontend system design plan covers durable ownership boundaries', () => {
const plan = createSystemDesignPlan('indexedDB pending-operation persistence');
expect(plan.boundaries.map((boundary) => boundary.owner)).toEqual([
'url',
'server',
'client',
'design-system',
'telemetry',
]);
expect(plan.performanceBudget.interactionResponseMs).toBeLessThanOrEqual(100);
expect(plan.rollout.some((item) => item.check.includes('feature flag'))).toBe(true);
});Interviewer Signal
The key phrase: frontend owns continuity; backend owns canonical records.
Constraints
- Keep local, backend, wallet, chain, and user-visible state distinct.
- Name the product risk before naming the component.
- Tie the answer back to testing or rollout safety.
Model Answer Shape
- Choose fields, expiry, schema versioning, conflict handling, and cleanup rules for pending transfer records.
- Use explicit ownership boundaries for state, data, and user intent.
- Describe how the UI prevents misleading certainty during pending or failed operations.
Tradeoffs
- Finance-grade UI should be conservative about certainty and optimistic about continuity.
- Local state improves recovery but must not pretend to be canonical business truth.
Edge Cases
- Refresh during pending work.
- Duplicate user intent.
- Backend, wallet, and chain disagree temporarily.
Testing And Proof
- State transition test.
- Reload recovery scenario.
- Accessible status and copy review.
Follow-Ups
- What would you log for support?
- How would you roll this out behind a flag?
Deep Finance Practice
This item has an authored finance specialization page with the original prompt, solution, and any available runnable harness.
Open legacy practice #311 ->