DEV Community

Cover image for Fintech Frontend Engineering: What You Need to Know Before Building in Financial Services
NorfolkD
NorfolkD

Posted on

Fintech Frontend Engineering: What You Need to Know Before Building in Financial Services

Most frontend engineers entering fintech underestimate how much the domain changes what good looks like. It's not just stricter security requirements or more complex forms. The regulatory, compliance, and payments context fundamentally reshapes how you architect UI, how you reason about state, how you handle errors, and what "done" means.

This post is a reference for frontend engineers — particularly those operating at staff level, where you're expected to contribute credibly beyond UI concerns — moving into or deeper into fintech. It covers the conceptual terrain: compliance constraints, payments flows, and the architectural patterns that emerge from them.

Why Domain Fluency Matters at the Frontend Layer

At staff level, you're expected to make architectural decisions that won't rot under you. In fintech, those decisions carry regulatory surface area. A poorly designed state machine for a payments flow isn't just a UX bug — it can result in double charges, incomplete settlements, or audit failures.

Frontend engineers who treat fintech as "the same job with stricter validation" tend to build systems that handle happy paths correctly but crack under the domain's actual failure modes:

  • Idempotency violations from optimistic UI updates
  • Race conditions between transaction state and UI state
  • Accessibility failures that violate regulatory obligations (WCAG is a legal requirement in financial services in several jurisdictions)
  • Audit trail gaps caused by insufficient event logging at the client layer

Domain fluency means you can anticipate these failure modes before they're filed as incidents.

The Compliance Layer Is an Architectural Constraint

In most product domains, compliance is a checklist you run through late in a feature cycle. In fintech, it's a first-class architectural input.

KYC and KYB Flows

Know Your Customer (KYC) and Know Your Business (KYB) verification flows are often the first complex frontend engineering problem you'll encounter. They look like multi-step forms, but they behave like state machines with external dependencies.

A KYC flow typically involves:

  1. Document collection (government ID, proof of address)
  2. Submission to a third-party verification provider (e.g., Persona, Onfido, Jumio)
  3. Asynchronous verification (taking minutes to days)
  4. Status polling or webhook-triggered state transitions
  5. Remediation loops for rejected submissions

The frontend architecture needs to handle the async nature explicitly. A naive implementation that treats verification as a synchronous form submission will fail badly. You need a durable state model:

type KYCStatus =
  | { stage: 'not_started' }
  | { stage: 'documents_pending' }
  | { stage: 'submitted'; submittedAt: string }
  | { stage: 'under_review' }
  | { stage: 'approved'; approvedAt: string }
  | { stage: 'rejected'; reason: string; retryAllowed: boolean }
  | { stage: 'manual_review' };
Enter fullscreen mode Exit fullscreen mode

This discriminated union pattern forces every rendering path to handle every state explicitly. When the backend team adds a new status, TypeScript surfaces every unhandled case in the UI. In regulated flows, "we missed a status" is not an acceptable postmortem entry.

Audit Trails and Immutability

Regulatory frameworks — PCI-DSS, SOX, GDPR, FCA rules — often require that financial actions be attributable, timestamped, and non-repudiable. This has direct implications for how you design frontend event logging.

Client-side events that touch payment initiation, consent recording, or document submission should be:

  • Emitted as structured, typed events
  • Correlated with a server-side trace ID
  • Stored immutably (no update or delete semantics at the log layer)

This is not analytics. It's an audit trail. Design it accordingly.

Payments Flows: The State Machine Problem

A payments flow is a distributed state machine with multiple participants: the user, your frontend, your backend, and a payments processor (Stripe, Adyen, Braintree). The frontend's job is to accurately reflect state transitions that it does not fully control.

Idempotency and Optimistic UI

Optimistic UI — updating local state before server confirmation — is a standard pattern for improving perceived performance. In payments, it's dangerous without careful design.

Consider a "Pay Now" button. If a user taps it and the network is slow, they may tap again. Without idempotency keys on the backend and request deduplication on the frontend, you risk two charges.

The correct pattern:

const usePaymentSubmit = () => {
  const idempotencyKey = useRef(crypto.randomUUID());
  const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');

  const submit = async (paymentDetails: PaymentDetails) => {
    if (status === 'submitting' || status === 'success') return;
    setStatus('submitting');

    try {
      await initiatePayment({
        ...paymentDetails,
        idempotencyKey: idempotencyKey.current,
      });
      setStatus('success');
    } catch (err) {
      setStatus('error');
      // Do NOT reset idempotencyKey — allow retry with the same key
    }
  };

  return { submit, status };
};
Enter fullscreen mode Exit fullscreen mode

The idempotency key persists across retries intentionally. If the first request succeeded but the response was lost in transit, a retry with the same key returns the original result rather than creating a second transaction.

Terminal vs. Non-Terminal States

Transaction states divide into terminal (no further transitions possible) and non-terminal (may still change). SETTLED and REFUNDED are terminal. PENDING and PROCESSING are not.

UI that treats a non-terminal state as final is incorrect. A payment showing PROCESSING needs a polling mechanism or a WebSocket subscription to eventually converge to SETTLED or FAILED. Users who close the browser and return need to see the current state, not a cached intermediate state.

Your state management layer needs to distinguish between local UI state and remote transaction state, and it needs a reconciliation mechanism:

// TanStack Query handles staleness and refetching cleanly
const { data: transaction } = useQuery({
  queryKey: ['transaction', transactionId],
  queryFn: () => fetchTransaction(transactionId),
  refetchInterval: (data) => {
    // Stop polling once we reach a terminal state
    const terminalStates = ['settled', 'failed', 'refunded', 'cancelled'];
    return data && terminalStates.includes(data.status) ? false : 2000;
  },
});
Enter fullscreen mode Exit fullscreen mode

Regulatory UI Obligations

Accessibility Is Not Optional

In the UK, the Equality Act 2010 requires that financial services be accessible. In the EU, the European Accessibility Act came into full force in June 2025. In the US, the DOJ has increasingly cited WCAG 2.1 AA as the standard for ADA compliance in financial services litigation.

WCAG compliance in fintech is a legal obligation, not a nice-to-have. A payment form with poor focus management, missing ARIA labels on error states, or keyboard traps isn't just a bad experience — it's a liability.

For complex flows (multi-step forms, modal confirmation dialogs, dynamic error states), you need to:

  • Manage focus programmatically on step transitions
  • Announce dynamic content changes via aria-live regions
  • Associate error messages with their inputs via aria-describedby
  • Test with actual screen readers, not just automated scanners

Consent and Disclosure UI

Many financial actions require explicit, recorded consent: terms of service acceptance, fee disclosures, risk warnings. The UI patterns here are legally significant.

A pre-checked checkbox, an auto-dismissing modal, or a disclosure hidden below the fold can each invalidate consent in regulatory terms. These components need compliance review, not just product review.

When building consent components, treat the consent event as a domain event with a typed payload:

type ConsentEvent = {
  type: 'consent_recorded';
  userId: string;
  consentType: 'terms_of_service' | 'fee_disclosure' | 'risk_warning';
  documentVersion: string;
  timestamp: string; // ISO 8601
  ipAddress: string; // captured server-side
  userAgent: string;
};
Enter fullscreen mode Exit fullscreen mode

The frontend initiates this event; the backend records it immutably.

Architecture Patterns That Emerge from Fintech Constraints

Strict Data Flow and Validation

Financial data has tight schemas. An amount field that accepts string when it should be number in minor currency units, or an ambiguously formatted date, can cause downstream failures in payment processing. Use Zod (or an equivalent schema library) to validate API responses at the boundary:

import { z } from 'zod';

const TransactionSchema = z.object({
  id: z.string().uuid(),
  amountMinorUnits: z.number().int().positive(),
  currency: z.string().length(3), // ISO 4217
  status: z.enum(['pending', 'processing', 'settled', 'failed', 'refunded']),
  createdAt: z.string().datetime(),
});

type Transaction = z.infer<typeof TransactionSchema>;
Enter fullscreen mode Exit fullscreen mode

This validation belongs at the API client layer, not scattered across components. When a backend team changes a field type, you catch it at the boundary — not in a production error report.

Feature Flagging and Progressive Rollouts

Financial features can't be safely rolled back mid-transaction. A user who has started a payment flow under version A of your app cannot safely complete it under version B if the flow structure has changed. Feature flags and cohort-based rollouts are therefore critical infrastructure, not optional.

Design new payment flows as entirely separate routes or experiences, keeping the old flow live until the rollout is complete and all in-flight transactions have resolved.

Sensitive Data Handling on the Client

Card numbers, bank account details, and government ID numbers should never pass through your application's JavaScript. Use iframe-based tokenization (Stripe Elements, Adyen Web Components) so sensitive data travels directly from the browser to the processor. Your app receives a token, not the raw data.

This isn't just a security best practice — it substantially reduces your PCI-DSS compliance scope.

What This Means for a Frontend Engineer in Practice

Building in fintech means accepting that:

  • Every flow has a failure mode that matters more than the happy path. Design for settled, failed, and pending, not just success.
  • State machines are load-bearing structures. Informal state management — boolean flags, nested conditionals — will break in a domain with this many states and transitions.
  • Compliance constraints are design constraints. Engage with them early, not at the end of a sprint.
  • The TypeScript type system is a compliance tool. Exhaustive unions and strict validation at API boundaries catch domain errors before they become incidents.

Frontend engineers who understand the domain can challenge product decisions, identify compliance risks in designs, and write code that reflects the actual semantics of financial operations. That's the difference between owning a UI layer and being a full contributor to a fintech product.

The investment in domain knowledge pays back quickly — and in fintech, the cost of not making it is measured in real money.

Top comments (0)