Ahmed Abdelaziz

FX Checker — a dark-themed live currency converter with ECB/EOD dataDesign Docs

Architecture Decisions

Four key choices and why they were made.

Last updated August 2026

Overview

Four decisions that shaped FX Checker. Each has context, choice, why, and trade-off — no jargon wall.


ADR-001: Server Actions as the Data Boundary

Status: Accepted

Context: The app is Next.js-only and calls a third-party FX API from client components via React Query.

Decision: Mark services/api.ts with "use server" and import its typed functions directly as queryFns — no Route Handlers, no API routes.

Why: Typed, colocated I/O with zero route boilerplate, and the fetch cache (revalidate) applies exactly at the data boundary.

Alternatives: Route Handlers under app/api/ (more boilerplate for one route), direct client fetches (exposes the data layer, loses caching).

Trade-off: Server actions only run when a client triggers them — fine for this single-route app, unsuitable if other services needed the same data.


ADR-002: Zustand + localStorage Instead of a Database

Status: Accepted

Context: Favorites, conversion log, and tab state should survive reloads — but the product has no accounts and no backend.

Decision: One central Zustand store; persist a partialized slice (favorites/log/tab) via the persist middleware to localStorage.

Why: A single store eliminates prop drilling; persistence is a two-line upgrade; user data stays private on-device and the server's attack surface stays zero.

Alternatives: Context + useReducer (rejected for frequent fine-grained reads), Postgres/Supabase or IndexedDB (overkill for zero-login scope).

Trade-off: No cross-device sync; localStorage is opaque to search. Acceptable for a personal tool.


ADR-003: Tailwind v4 Inline Theme Tokens on an 8px Grid

Status: Accepted

Context: A dark palette, lime accent, and tight consistent spacing define the design system.

Decision: Anchor spacing tokens on an 8px grid (p-100 = 8px through p-1600 = 128px, with a few half-steps for fine control) and map raw CSS variables to semantic utilities (bg-currency-field-bg) inside @theme inline.

Why: Token names communicate absolute values at a glance; semantic color names insulate components from palette changes.

Alternatives: Default Tailwind scale, arbitrary values everywhere, per-component CSS files.

Trade-off: New spacing steps require touching the token table; occasional inline values (rounded-[8px]) where no token exists.


ADR-004: Single-Round-Trip Ticker with Business-Day Splitting

Status: Accepted

Context: The marquee needs today's and the previous business day's rates per pair; two ?date= calls returned inconsistent windows after weekends.

Decision: Fetch a 7-day range once, split the flat response by distinct date into "latest" and "second-latest" business days, then join pairs.

Why: One upstream call instead of two, stable cache keys, correct day-over-day deltas across weekends and holidays.

Alternatives: Two ?date= calls (the previous behavior — inconsistent), per-pair history calls (multiplies requests).

Trade-off: Larger range payload than a single-date response — absorbed by the 1-hour server cache and invisible to clients.

Common thread

Cache politely at the boundary, keep state in one place, make tokens carry meaning, and let the data's real shape (business days) drive the logic.