Free

2026-09-05

architecturescalingenterprisecomponent-systems

GDS: what 80 components for global banks taught me (NDA-safe)

Working on 80+ components for global financial institutions taught me lessons that go far beyond typical web component development. While I can't share specific client details, the architectural patterns and insights are universally applicable.

# The banking context: zero tolerance for failure

Global banking systems operate under constraints that make web development look trivial:

  • 99.999% uptime requirements (5.26 minutes of downtime per year)
  • Sub-100ms response times for customer-facing interfaces
  • Multi-regional deployment with failover capabilities
  • Stringent accessibility requirements (WCAG 2.1 AA minimum)
  • Audit trails for every user action

These constraints forced us to design components that were not just functional, but operationally resilient.

# Component architecture lessons

# 1. State machines over state management

In banking, UI state isn't just about user experience—it's about data consistency and auditability. We learned that finite state machines provide:

dialog-state-machine.ts
// Instead of complex state objects
interface ComplexDialogState {
  isOpen: boolean;
  isLoading: boolean;
  error: string | null;
  data: any;
}
 
// We used explicit state machines
enum DialogState {
  Idle,
  Opening,
  Loading,
  Success,
  Error,
  Closing
}
 
// Each state transition is auditable and predictable
function transition(state: DialogState, action: DialogAction): DialogState {
  const transitions = {
    [DialogState.Idle]: {
      open: DialogState.Opening
    },
    [DialogState.Opening]: {
      complete: DialogState.Loading,
      error: DialogState.Error
    }
    // ... explicit transitions for every state
  };
  
  return transitions[state]?.[action] || state;
}

# 2. Component composition patterns

Banking applications require extreme composability. We discovered three essential patterns:

# a. Container/presenter separation

account-balance.tsx
// Container handles logic and state
function AccountBalanceContainer({ accountId }) {
  const [balance, setBalance] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetchBalance(accountId).then(setBalance);
  }, [accountId]);
  
  return <AccountBalancePresenter balance={balance} loading={loading} />;
}
 
// Presenter handles pure rendering
function AccountBalancePresenter({ balance, loading }) {
  if (loading) return <LoadingSpinner />;
  return <CurrencyDisplay amount={balance} />;
}

# b. Render prop patterns for complex interactions

account-transfer-form.tsx
function AccountTransferForm({ children }) {
  const [state, transition] = useAccountTransferMachine();
  
  return children({
    state,
    actions: {
      initiate: () => transition('initiate'),
      validate: (data) => transition('validate', { data }),
      confirm: () => transition('confirm'),
      cancel: () => transition('cancel')
    }
  });
}
 
// Usage
<AccountTransferForm>
  {({ state, actions }) => (
    <TransferForm current={state} onSubmit={actions.confirm} />
  )}
</AccountTransferForm>

# c. Higher-order components for cross-cutting concerns

with-audit-log.tsx
function withAuditLog(WrappedComponent) {
  return function AuditedComponent(props) {
    const [auditTrail, setAuditTrail] = useState([]);
    
    const enhancedProps = {
      ...props,
      actions: {
        ...props.actions,
        audit: (action, data) => {
          setAuditTrail(prev => [...prev, { action, data, timestamp: Date.now() }]);
          props.actions?.[action]?.(data);
        }
      }
    };
    
    return <WrappedComponent {...enhancedProps} />;
  };
}

# Performance at scale

# 1. Component virtualization

With 80+ components on a single dashboard, we had to implement intelligent rendering:

component-grid.tsx
// Virtualized component loader
function ComponentGrid({ components }) {
  const [visibleRange, setVisibleRange] = useState({ start: 0, end: 10 });
  
  const handleScroll = useCallback((e) => {
    const scrollTop = e.target.scrollTop;
    const start = Math.floor(scrollTop / 100); // 100px per component
    const end = start + 10;
    setVisibleRange({ start, end });
  }, []);
  
  return (
    <div onScroll={handleScroll} style={{ height: '1000px', overflow: 'auto' }}>
      {components.slice(visibleRange.start, visibleRange.end).map(Component)}
    </div>
  );
}

# 2. Memoization strategies

Banking dashboards have complex state interactions. We learned to memoize aggressively:

account-summary.tsx
// Component-level memoization
const AccountSummary = React.memo(({ account, transactions }) => {
  const calculatedData = useMemo(() => {
    return {
      total: transactions.reduce((sum, t) => sum + t.amount, 0),
      latest: transactions[0],
      trends: calculateTrends(transactions)
    };
  }, [transactions]);
  
  return <AccountSummaryView {...account} {...calculatedData} />;
});
 
// Selector-based memoization
const useSelector = (selector) => {
  const state = useContext(StateContext);
  return useMemo(() => selector(state), [state, selector]);
};

# Accessibility beyond compliance

Banking accessibility goes beyond WCAG. We had to support:

# 1. Screen reader announcements for state changes

use-announcements.ts
function useAnnouncements() {
  const [announcements, setAnnouncements] = useState([]);
  
  const announce = useCallback((message, priority = 'polite') => {
    const id = Date.now();
    setAnnouncements(prev => [...prev, { id, message, priority }]);
    
    setTimeout(() => {
      setAnnouncements(prev => prev.filter(a => a.id !== id));
    }, 1000);
  }, []);
  
  return { announcements, announce };
}
 
// Usage
const { announce } = useAnnouncements();
const handleTransfer = () => {
  announce('Transfer initiated. Please confirm amount.');
  // ... transfer logic
};

# 2. Keyboard navigation for complex workflows

use-keyboard-navigation.ts
const useKeyboardNavigation = (items) => {
  const [focusedIndex, setFocusedIndex] = useState(0);
  
  const handleKeyDown = (e) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setFocusedIndex(prev => (prev + 1) % items.length);
        break;
      case 'ArrowUp':
        e.preventDefault();
        setFocusedIndex(prev => (prev - 1 + items.length) % items.length);
        break;
      case 'Enter':
        e.preventDefault();
        items[focusedIndex].action();
        break;
    }
  };
  
  return { focusedIndex, handleKeyDown };
};

# Monitoring and observability

With 80 components, we needed granular monitoring:

with-performance.tsx
// Component performance monitoring
function withPerformance(Component) {
  return function MonitoredComponent(props) {
    const renderStart = useRef(performance.now());
    const [metrics, setMetrics] = useState(null);
    
    useEffect(() => {
      const renderEnd = performance.now();
      const renderTime = renderEnd - renderStart.current;
      
      setMetrics({
        renderTime,
        timestamp: Date.now(),
        component: Component.displayName
      });
      
      // Send to monitoring service
      trackComponentPerformance({
        component: Component.displayName,
        renderTime,
        timestamp: Date.now()
      });
    }, []);
    
    return <Component {...props} />;
  };
}

# Key takeaways

  1. State machines provide predictability in complex financial workflows
  2. Container/presenter separation enables better testing and maintenance
  3. Aggressive memoization is essential for performance with many components
  4. Accessibility must be proactive, not just compliant
  5. Monitoring at the component level helps identify performance bottlenecks
  6. Composability isn't optional—it's essential for enterprise applications

These lessons from global banking systems apply to any large-scale component system, whether it's fintech, healthcare, or enterprise SaaS. The key is designing for operational resilience from day one.