None of the work in this post shows up on a product roadmap. It's a tooling migration, some dependency bumps and a telemetry schema: the kind of thing that's easy to describe and hard to defend in sprint planning. It's also most of what decides whether a codebase is pleasant or miserable to work in three years from now.
Codebase health is a lagging indicator. You either pay the maintenance cost in small steady amounts, or you pay a bigger one later in debugging, security incidents and onboarding pain. What follows is the small steady version.
One toolchain instead of two
We moved a set of frontend services from ESLint and Prettier to Biome. ESLint and Prettier have been the default frontend linting and formatting stack for years, and they work fine. The friction is in running two separate tools: separate config files, separate plugin dependencies, the occasional conflict where Prettier reformats something ESLint just fixed, and a growing pile of wrapper packages and shared configs to keep consistent across repos.
Biome is one tool that does both. It's written in Rust, so it's noticeably faster than the ESLint and Prettier combination on larger codebases. It ships with sensible defaults, needs little configuration, and deliberately has no plugin system. For a team running several services, the draw isn't really the raw speed. It's the smaller configuration surface you have to keep consistent across all of them.
The migration was largely mechanical, and roughly the same in each repo:
- Remove ESLint, Prettier, and their config files and plugins
- Install Biome with a shared config inheriting from a common preset
- Run the formatter over the codebase and commit the diff
- Address the handful of lint rules where Biome's defaults differ from the old setup
In parallel, test tooling moved to a shared internal Jest preset, so repos that used to configure Jest themselves now inherit a standard setup. Less config to maintain, and a consistent test environment across services.
Giving up the plugin ecosystem is a real trade, and worth being honest that it's a trade rather than a free win: a plugin system is also the thing that lets you add exactly the rule your codebase needs. What we got back for it was fewer breaking changes to navigate and noticeably easier maintenance when it comes to updating dependency versions. On a stack spread across several repos, the tool you never have to think about is worth more than the rule you occasionally wish you had.
The formatting diffs were large on some repos, thousands of lines, but purely cosmetic. That's the usual worry with a mechanical formatting change: a noisy diff makes review harder and can bury real changes in whitespace churn. In practice it wasn't a problem. The diff views in GitHub and in the editor made the changes easy to read, especially where commits and pull requests stated plainly what they were for. Keeping formatting commits separate from functional ones is what makes that work: the tooling can only help you if you haven't mixed the two together in the first place.
The debt that came along for the ride
Touching these repos was also a chance to clear some accumulated dependency debt. The monorepo tooling in one repo was pinned to a version with known security advisories. Nothing actively exploited, just the sort of thing that keeps turning up in scans until you deal with it, so it went to the current stable release. A few repos had Node version pinning in CI sitting behind current LTS, updated at the same time.
None of this is interesting work. But leaving it to pile up means that six months from now, someone's investigating a security advisory under time pressure, or upgrading the monorepo tooling as an emergency instead of a planned change.
Saying where an error came from
The same instinct applies to observability, where most problems aren't really about the tools. They're about the data flowing into them being imprecise. Fixing that is duller than picking a dashboard, and it matters more.
Two services both handle PNR retrieval, the booking record at the heart of any reservation, and both had gaps. The first was missing structured telemetry logging entirely for its main flow. The fix was to add logging at each step, mapped to a defined schema. The schema is the whole point. Without one, every team reading the logs interprets freeform text its own way, and tooling can't aggregate anything reliably. Define it, and you can build dashboards, alerts and anomaly detection on top instead of reading log lines by hand.
The second was subtler. A service was surfacing errors from an upstream service layer without saying where they came from, so from the outside a failure looked like it originated in the retrieval service itself. The fix was to tag anything coming from upstream with the right source identifier in the telemetry, so an engineer debugging a failure can tell straight away whether the problem is theirs or upstream's.
The same idea turns up in static error classification, keeping an origin enum in sync, but here it's about what the logs say at runtime. In principle the two could disagree, the enum saying one thing and the telemetry another, though we've yet to see a case where they do.
The payoff is unglamorous and immediate: with honest, clearer logging we triage, identify and debug errors more quickly. Knowing where a failure actually came from costs nothing when everything's working, and is most of the job when it isn't.
The compounding bit
The value of standardisation work is almost invisible while you're doing it. Biome configured the same way everywhere, Jest set up identically, the advisories cleared, the logs saying which service a failure came from. Day to day, none of it feels like much.
It shows up later. Someone new moves between services without relearning the config. An upgrade goes through mechanically because everything follows one pattern. A lint rule catches a real bug that looser checking would have waved through. An engineer reads one log line and stops debugging the wrong service.
There's one piece of this I've left out, because it turned into a bigger subject than a section. Feature flags accumulate faster than they get cleaned up, and I've been building an AI-assisted flow to make removing them tractable. The automation was the easy part; deciding which flags it's allowed to touch was not. That's a subject of its own, and one I'll come back to shortly.
The trick with all of it is keeping this kind of work visible enough that it actually gets done, instead of being crowded out by features every sprint. It never argues for itself, because the incident it prevents is one you don't have and can't point at.
Responses