The repo flag that lets me run any microservice locally (and the day it hid a real bug)

I added a simple --local flag to every microservice to cut setup time. It saved afternoons — until it masked a production bug and forced a stricter rule set.

Written by: Arjun Malhotra

A developer typing on a laptop at a cluttered desk with notebook and coffee
Photo by Clem Onojeghuo on Unsplash

It was 9:40 on a wet Bengaluru morning and I still hadn’t reproduced the bug our QA team reported. Every microservice involved meant starting containers, waiting for DB migrations, juggling ports, and cursing my laptop’s fan. I missed a meeting. I lost focus. I lost an hour that could have fixed the bug.

So I added a flag: —local. Run the service with it and it would boot with in-memory stubs, an embedded SQLite, and a couple of sensible defaults. No Kafka. No Redis. No key-management calls. No docker-compose. Within 20 seconds the service would be up and I could iterate on the code that mattered.

This is how I implemented it, why it worked, and the hard lesson I paid for.

Why a flag (and not a mock server or full infra) I tried two other routes first.

The flag felt like a local-first compromise. Each repo owned its small local-mode logic. The developer experience was predictable: git checkout, make dev, ./service —local, edit, retry. No context-switching. No special account credentials. Faster feedback loops.

What —local does (the practical bits) I kept the implementation tiny — the benefit of small, owned surface area.

I documented the caveats in README.md: local mode is for development, never for performance testing, and not for long-lived state.

Why it actually saved time The gains were immediate and real.

It changed how we debugged — small loop, immediate assertions, and I stopped bringing up the whole stack for every trivial change.

The failure: the bug it hid Three months later, a high-severity payment failure hit production. Our logs showed a race condition between two services that only occurred under real network latency and when the payment gateway returned a certain timeout code — behavior our local fake never reproduced.

The bug trace led to the local-mode implementation.

We patched production fast. Then we had the uncomfortable discussion: we had increased developer velocity at the cost of eroding coverage for critical edge cases.

The tradeoffs I accepted (and later tightened) I wasn’t willing to throw away local-mode — it solved real, everyday pain. But I had to stop treating it as a silver bullet.

Changes we implemented:

An honest limitation Local-mode cannot simulate real network timing and cross-service ordering perfectly. It will never replace periodic integration runs that exercise real queues, timeouts, and third-party quirks. We learned this the hard way when a ₹2.4 lakh transaction path briefly misbehaved in production.

If your team has strict SLAs or deals with payments, treat local-mode as a developer convenience, not as an acceptance criterion.

When I still reach for —local I use it when I need to:

When I don’t I avoid —local for final verification, performance work, or anything involving money flows and external callbacks.

What I walked away with Local-mode is a power tool. It gives you minutes back in your day. But power tools need rules.

We kept the flag. We added CI safety nets and a habit: “If it touches money or async ordering, run an integration check.” That single rule prevented another 2 a.m. page and made the tradeoff worth it.

If you add a similar flag to your repos, make the fakes as simple as possible, version them with the repo, and protect the risky paths with automated tests that run outside local-mode. That tiny discipline saved me more than the flag ever did.