Building software for government is unlike any other domain. The stakes are higher, the constraints are tighter, and the users are everyone — not a curated segment of early adopters.
After 18+ years working across HMCTS, the Ministry of Justice, the Department for Transport, and DWP, I’ve collected a set of principles that separate the projects that ship from the ones that stall.
1. Start with the user, not the service
GOV.UK’s first design principle — “Start with needs” — sounds obvious. It rarely is in practice. On almost every engagement, the initial brief describes a system rather than a user problem.
When we started on the Possession Claims Service for HMCTS, the instinct was to replicate the paper process digitally. Instead, we spent the first sprint mapping who was actually using the service: solicitors filing in bulk, litigants in person filing once, judges reviewing evidence. Three completely different mental models, one service.
// Mapping user journeys before writing a line of code
interface UserJourney {
persona: 'solicitor' | 'litigant-in-person' | 'judge';
entryPoint: string;
primaryTask: string;
painPoints: string[];
}
const journeys: UserJourney[] = [
{
persona: 'solicitor',
entryPoint: 'bulk upload CSV',
primaryTask: 'file 50+ claims per week efficiently',
painPoints: ['repetitive data entry', 'no batch status updates'],
},
{
persona: 'litigant-in-person',
entryPoint: 'GOV.UK search',
primaryTask: 'understand what I need to do',
painPoints: ['legal jargon', 'no guidance on documents needed'],
},
];
The resulting service had three distinct flows. Every content decision, every form field, every error message was written for a specific persona.
2. Treat accessibility as architecture, not audit
Most teams bolt accessibility on at the end. A quick axe scan, a few colour fixes, ship it. This doesn’t work — and in government it’s not optional. WCAG 2.2 AA compliance is a legal requirement under the Public Sector Bodies Accessibility Regulations.
The teams that do this well bake it into the Definition of Done:
- Every new component has been tested with a screen reader (NVDA + Chrome, VoiceOver + Safari)
- Keyboard navigation is verified before PR approval
- Colour contrast is checked at design stage, not dev stage
// Design tokens that guarantee contrast ratios
// — checked against WCAG 1.4.3 (contrast ratio ≥ 4.5:1 for normal text)
$gov-text-on-white: #0b0c0c; // 21:1 — always safe
$gov-link: #1d70b8; // 4.54:1 on white — just passes
$gov-link-visited: #4c2c92; // 5.0:1 on white — passes
$gov-focus: #ffdd00; // High-vis focus ring
3. CI/CD is not optional on government projects
Government services have some of the strictest change-management requirements around. Paradoxically, that’s a reason to invest heavily in automation — not to move fast, but to prove that every change is safe.
On our DfT engagement we built a pipeline that enforced:
# Simplified CI pipeline for a GDS-style service
stages:
- lint
- unit-test
- integration-test
- accessibility-audit
- security-scan
- deploy-to-staging
- smoke-test
- deploy-to-prod
accessibility-audit:
script:
- npx axe-cli https://staging.service.gov.uk --tags wcag2a,wcag2aa
allow_failure: false # Hard gate — accessibility failures block the release
The accessibility audit was a hard gate. A failing axe scan blocked the release. No exceptions, no overrides without a documented risk acceptance from the product owner.
4. The strangler fig for legacy modernisation
Every government department has a 20-year-old mainframe system somewhere. The strangler fig pattern — gradually routing traffic to a new service while keeping the legacy system alive — is the safest way to modernise without a big-bang rewrite that risks public services.
The key is building a routing layer early:
// Simplified strangler fig routing middleware
import type { Request, Response, NextFunction } from 'express';
const featureFlags = {
useNewClaimsService: process.env.NEW_CLAIMS_ENABLED === 'true',
useNewPaymentsFlow: process.env.NEW_PAYMENTS_ENABLED === 'true',
};
export function routerMiddleware(req: Request, res: Response, next: NextFunction) {
if (req.path.startsWith('/claims') && featureFlags.useNewClaimsService) {
return next(); // New service handles it
}
// Fall through to legacy proxy
res.redirect(307, `${process.env.LEGACY_BASE_URL}${req.path}`);
}
Key takeaways
- User research is not a phase — it’s a continuous activity woven through delivery
- Accessibility is architecture — design for it, test for it, gate on it
- Automate everything auditable — if it can be checked by a machine, it should be
- Strangle the legacy — don’t rewrite, re-route
Government digital delivery is hard. But when it works, the impact is immense — millions of people interacting with services that actually serve them. That’s worth getting right.