โ† Back to question bank
System DesignSeniorHard#522 ยท 35mFinance specialization

Design feature flags for a risky financial UI rollout

Cover cohort rollout, kill switches, audit logging, analytics, fallbacks, and stale flag cleanup.

Answer Strategy

Treat feature flags for a risky financial UI rollout 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 feature flags for a risky financial UI rollout 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.

Requirements
Write acceptance criteria for the primary workflow, non-goals, permissions, device constraints, and freshness expectations.
Contracts
Add API adapter contract tests and reducer tests for URL, server, and local interaction state.
Experience
Cover loading, empty, partial-data, offline, keyboard, and screen-reader states in component stories or interaction tests.
Operations
Ship behind a flag, watch client error rate and workflow abandonment, and define rollback before launch.
test('frontend system design plan covers durable ownership boundaries', () => {
  const plan = createSystemDesignPlan('feature flags for a risky financial UI rollout');

  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

Tie this to migration safety and beta rollout experience.

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

  • Cover cohort rollout, kill switches, audit logging, analytics, fallbacks, and stale flag cleanup.
  • 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 #522 ->