2026-09-05
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:
// 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
// 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
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
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:
// 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:
// 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
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
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:
// 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
- State machines provide predictability in complex financial workflows
- Container/presenter separation enables better testing and maintenance
- Aggressive memoization is essential for performance with many components
- Accessibility must be proactive, not just compliant
- Monitoring at the component level helps identify performance bottlenecks
- 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.