Only Load the Rules That Apply: A Postcode-Driven Config Loader for UK Services
Photo: Basile Morin, CC BY 4.0, via Wikimedia Commons
Every UK service ends up with a file called something like region-rules.js. It starts with three conditionals for Scotland and Wales. Two years later it is 400 lines, half of it dead, and the content team cannot change a single sentence without a release.
Splitting those rules out is not a framework rewrite. It is a lookup, a fetch and a cache — plus a bit of care around the border cases that make UK geography entertaining.
Why the rules should not ship with the app
Bundled rules have two problems. They make your main bundle heavier for people in Cornwall who will never see the Scottish rules, and they tie content changes to deploys. A recycling calendar change in April should not require a pull request from a developer who is on leave.
Move the rules into versioned JSON on a CDN, keep a small default in the app for the unhappy path, and both problems mostly disappear.
Decide what 'region' actually means first
This is where most implementations go wrong. Nation (England, Scotland, Wales, Northern Ireland) is not the same thing as postcode area. The CH area covers Chester in England and parts of Flintshire in Wales. LL stretches across the border near Wrexham and Oswestry. SY and NP do the same trick further south.
So pick your grain deliberately:
- Outward code (the bit before the space) for coarse rules like delivery surcharges or default opening hours.
- Administrative area from a lookup for anything legal or financial — council tax, deposit schemes, licensing.
- A hand-curated mapping when the rules follow a boundary that postcodes do not respect, such as a national park or a combined authority.
Write that decision down. It will save an argument in eighteen months.
The loader in three steps
Resolve, fetch, cache. Roughly this:
const BASE = 'https://cdn.example.co.uk/region-config/v3';
const memo = new Map();
export async function loadRegionRules(postcode) {
const key = normalise(postcode); // 'll11 2ab' -> 'LL112AB'
if (!key) return FALLBACK;
if (memo.has(key)) return memo.get(key);
const area = await resolveArea(key); // 'W06000006' or similar
const rules = await fetch(`${BASE}/${area}.json`, { cache: 'force-cache' })
.then(r => (r.ok ? r.json() : FALLBACK))
.catch(() => FALLBACK);
memo.set(key, rules);
return rules;
}
The obvious optimisation is to key the memo on the outward code where only coarse rules are needed, so LL11 2AB and LL13 9XX share a fetch. Keep a separate, finer cache for the legal stuff.
Choosing a lookup API
A quick and deliberately unscientific comparison of the usual options:
| Option | Shape | Good for | Watch out for |
|---|---|---|---|
| postcodes.io | Free, open data, bulk endpoints | Resolving postcodes to administrative areas | Community-run, no SLA, rate limits on public instances |
| Ideal Postcodes | Paid, per-lookup pricing | Address search plus rooftop coordinates | Costs scale with volume; check current terms |
| getAddress | Paid, autocomplete-first | Forms where typing speed matters | Less useful if you only need area codes |
| OS Places API | Licensed, authoritative | Addresses plus UPRN, all four nations | Licensing and pricing need reading properly |
| Royal Mail PAF | Licensed dataset | The reference source | Display and storage restrictions; not a casual dependency |
Check pricing and licensing yourself before committing — these things shift. For area resolution alone, a free open dataset is usually enough. For address capture, pay for it.
Cache at the edge like you mean it
Version the folder. /region-config/v3/ changes only when the config schema changes, and individual files inside it are immutable. Set a long max-age, add stale-while-revalidate, and let the CDN do the heavy lifting. Regenerating a config means writing a new file and bumping an index, not purging a cache blindly at 4pm on a Friday.
Keep config files small — a few kilobytes each. If one grows past 20KB, it is probably holding data that belongs in a database query, not a static file.
Fail safe, not fast
Never block rendering on the postcode lookup. Show the generic version of the page, mark anything region-specific as unconfirmed, and retry once in the background. A user in Dundee seeing an English default with a small note is a much better outcome than a blank form and a support ticket.
Log the failure with the outward code only, and alert if the failure rate crosses a threshold. Otherwise you will discover the API outage from a tweet.
Border cases worth a test card
- LL, CH, SY, NP — postcode areas that straddle the England–Wales boundary.
- HS, ZE, KW, PA — Scottish islands, where delivery and council tax assumptions break quickly.
- BT — Northern Ireland, a different regulatory regime in several respects.
- JE, GY, IM — Crown Dependencies, not part of the UK postcode system; reject politely rather than guessing.
- BF1 — British Forces Post Office addresses, which rarely behave.
- GIR 0AA — the one postcode that defeats most regular expressions.
- Whitespace, lowercase, outward code only, and a trailing newline pasted from a spreadsheet.
Build those into an automated test and you will catch most of the silly bugs before a user in Stornoway does.
Measure what you added
Track the config fetch latency at p95, the cache hit ratio at the CDN, and the bytes per region bundle. Prefetch on blur of the postcode field so the fetch overlaps with typing the rest of the form. If the numbers look bad, the answer is usually coarser rules or a warmer cache, not a bigger framework.