Your Paved Road Starts at git push

TL;DR - Ask a platform team what their road covers and they will describe everything that happens after git push: the pipeline, the deploy, the dashboards, the rollback. Ask what happens before the push and you get a link to a README. That is backwards, because the inner loop - clone, run, change something, see it work - is where developers spend most of their day, and it is the one part of the job every engineer pays for separately, in every repository, every time they move teams. A three-day local setup is not a rite of passage. It is a platform defect with no owner, documented in a file nobody has followed from scratch in a year. Pave it the same way you paved the rest: one command from the template that takes a clean machine to a running service, an explicit decision for every dependency (run it, fake it, or reach a shared one), local credentials from identity rather than a .env pasted in Slack, seed data that actually exists, and a measured clone-to-running time that a stranger can hit.

I build and run platforms for a living, and when I start with a new team I ask to do one thing before I read any architecture document. I ask to clone the most important service and get it running on my laptop, alone, using only what is written down.

It is the most reliable diagnostic I have, and almost nobody enjoys watching it. The README says to install a runtime version that was retired last spring. The setup script assumes a tool the author happened to have. There is an environment file to “get from someone on the team”. The service starts, then dies, because it wants a message broker and a second service and a database with a schema nobody has published. By lunchtime somebody kind has shared their screen and said “oh, you also need to…”, and by the end of the day it works, on my machine, in a way that nobody else could reproduce from the docs.

Then I walk across to the platform team and they show me a beautiful pipeline.

The platform road starts at git push versus a paved inner loop

The tell

Three questions, about the service your teams touch most:

  • How long does a competent engineer who has never seen this repository take to get it running locally, from a clean machine, using only the docs? Not someone on the team. A stranger. If nobody knows, nobody has tried, and the number is almost certainly measured in days.
  • After changing one line, how long until they can see the effect? The edit-to-feedback time on a laptop. If it involves a rebuild of a large image, a restart of five containers, or pushing to a branch to “see if it works in dev”, the inner loop has quietly been outsourced to the pipeline.
  • Where do local credentials come from? If the answer is a .env file passed around in a chat thread, a shared developer account, or “ask Marco”, the inner loop is running on secrets nobody can rotate.

If those three answers are vague, the road does not start where the developer starts. It starts where the platform team’s interest starts.

Why the road starts at the push

Nobody decided to leave the laptop out. It happens for reasons that each sound sensible.

  • The push is where the platform can see. Everything after git push runs on infrastructure the platform team owns, observes and gets paged for. Everything before it runs on a laptop they cannot see, with an operating system they did not choose. Teams pave what they can observe.
  • Local setup feels like the team’s business. Every repository is different, the team knows its own service, and dictating how people run code on their own machines sounds like overreach. So each team writes its own README, and two hundred teams solve the same problem two hundred times, badly.
  • The cost is paid once per person, so it looks small. Three days of setup per new joiner looks like a rounding error. It is not paid once. It is paid by every engineer for every repository they ever need to touch: the cross-team fix, the incident in a service you do not own, the move to a new team, the dependency you need to debug.
  • The pipeline became the development environment. When local is hard, people stop running things locally and push to see what happens. That turns a five-second loop into a fifteen-minute one, and turns a build everyone owns and nobody owns the total of into the only place anyone can run their code at all.

That last one is the expensive one. A broken inner loop does not stay on the laptop. It leaks into CI queue time, into a shared dev environment that is permanently broken because everyone tests in it, and into bigger, less-tested changes.

The arithmetic

The pillar’s test for a real platform is whether a new engineer can ship a hello-world service to production in a day. It is a good test, and it has a blind spot: it measures a new service, created from the template, with nothing in it. The engineer’s actual first week is spent on an existing service, built three years ago, which is the case the test never covers.

Do the sum once with your own numbers. Count the times last quarter that an engineer had to get a repository running that they had not run before: new joiners, internal moves, cross-team changes, incident responders, reviewers who wanted to try a branch. Multiply by your honest median setup time. In every organisation of more than a hundred engineers where I have done this, the figure is a few engineer-years per year, spent on the least interesting work there is, and it never appears in any plan because it is spread thinly across everyone.

And then the part that does not show up in the sum: the fixes nobody makes. A developer who sees a bug in a neighbouring service and knows it will take two days to run it does not fix the bug. They file a ticket. Cross-team contribution, the thing every platform strategy says it wants, dies quietly at the README.

Pave it from the template

The structural fix is the same one that worked everywhere else in the cluster: make the road start earlier, and generate it rather than document it.

  • One command, from a clean machine, to a running service. make dev, ./dev up, whatever name you like, but one, and the same one in every repository on the road. It installs or checks the toolchain, starts what the service needs, loads seed data, and prints the URL. If it fails, it fails with a message that names the fix. The README describes this command and nothing else.
  • The toolchain is declared, not described. Runtime versions, build tools and CLIs pinned in a file the tooling reads - a version manager file, a dev container definition, a Nix shell, pick one and pick it for the whole road. “Install Node 20” in prose is a bug report from the future.
  • The template ships it, and the platform owns the guts. The dev command belongs in the first golden path alongside the pipeline and the dashboards, and it should be a thin template over fat shared primitives: a few lines in each repository that call a shared, versioned tool. When the platform fixes the Apple Silicon problem or the new VPN breaks something, every repository gets the fix without two hundred pull requests.
  • Existing services get it by migration, not by announcement. The template only helps services created after it. For the estate you already have, the platform team opens the pull requests that add the dev command to the twenty most-touched repositories itself, the same way it pays the switching cost anywhere else.

Decide every dependency on purpose

The hard part of local development is never the service. It is everything the service talks to. Most setups fail here because nobody decided anything, so each developer improvises a different answer.

For every dependency, make one of three choices, write it down in the repository, and make the dev command implement it:

  • Run it locally. The database, the cache, the broker: real engines in containers, the same major version as production, started by the dev command. Cheap, fast, and honest enough for most work.
  • Fake it. A stub or a recorded contract for the other team’s API, the payment provider, the email service. Fakes are fine as long as they are owned. A fake nobody updates is a staging environment that lies, only smaller and on your laptop.
  • Reach a shared one. Some things are too big or too stateful to run locally: the search cluster, the data platform, a third-party sandbox. Then the dev command connects to a shared non-production instance, through the platform’s normal access path, and says so out loud when it does.

The anti-pattern is the fourth, undeclared option: “run the whole system locally”, fifteen containers and a fan you can hear from the next desk. If a service genuinely cannot be developed without running ten others, that is not a tooling problem. It is a coupling problem, and the honest response is to say so rather than to buy bigger laptops.

Credentials and data are the parts nobody paves

Two things break local setups more than any toolchain does, and both are platform problems wearing a team-problem costume.

  • Local credentials come from identity. The developer logs in once with their normal identity, and the dev command fetches short-lived credentials for the non-production resources they are allowed to reach. No long-lived keys in a .env, no shared developer accounts, no secret values in a chat history. This is the same argument I made about distributing identity rather than secret values, applied to the one environment that security reviews usually forget exists. A .env passed around in Slack is a secret the platform cannot rotate and the security team does not know about.
  • Seed data exists and is maintained. An empty database is not a development environment. Ship a seed script or a small anonymised dataset with the service, owned by the team, run by the dev command, and exercised in CI so it cannot rot unnoticed. If you have a paved road for the data layer, the same machinery that restores production into a scratch environment can produce a scrubbed, small, realistic copy for local use. Never production data on a laptop.

Who owns what

As with every shared capability, draw the line on purpose:

  • The platform owns the mechanism. The dev command, the toolchain declaration format, the shared tool behind it, local credential issuance, the containerised versions of the standard dependencies, and the clone-to-running measurement.
  • The team owns the meaning. Which of the three choices each dependency gets, the fakes for their own APIs that other teams consume, the seed data, and keeping their repository green on the “stranger test”.
  • Anyone who publishes an API publishes a way to fake it. If your service is a dependency of twenty others, a maintained local stub or contract is part of the service, not a favour.

That last rule does more for cross-team development than any amount of documentation.

How you know it is working

  • Clone-to-running for a stranger, per repository. Measured, not estimated. The best way is automated: CI runs the dev command on a clean runner every night and fails loudly when setup breaks. The human version is a new joiner or a rotating volunteer timing it once a quarter on the most-touched repositories.
  • Edit-to-feedback time on the laptop. For the common change, seconds. If it is minutes, the hot-reload or incremental build is broken, and people will drift back to pushing to find out.
  • Share of repositories where the dev command works from clean. The honest coverage number. Not “repositories with a README”.
  • Pushes that exist only to test. Commits titled “try again”, “debug” and “does this work” on feature branches. A rough signal, but a revealing one: each is someone using the pipeline because local does not work.
  • Credentials in the wild. How many long-lived non-production keys exist on developer machines. The number should be falling towards zero.

Notice what is not on the list: developer satisfaction surveys about tooling. Run them if you like, but the clean-machine test tells you the same thing faster and cannot be answered politely.

When this is the wrong answer

  • You have one monolith and it already works. If every engineer works in the same repository and setup is one command that nobody complains about, keep it working with the nightly clean-machine run and stop there. You have already paved this road.
  • Twelve engineers. At that size the README plus someone who answers questions is a perfectly good platform. Write the setup down honestly, run it from scratch when someone joins, and revisit when the number of repositories outgrows the number of people.
  • The services genuinely cannot run locally. Some systems, especially heavy data or ML workloads, cannot be developed on a laptop at any reasonable cost. Then the inner loop belongs in remote or ephemeral environments, and every rule above applies there instead: one command, declared dependencies, identity-based access, seed data, a measured time to running. Moving the loop to the cloud does not exempt it from being paved.
  • Locked-down or regulated machines. If engineers cannot run containers or install toolchains locally, do not fight the policy. Provide the remote environment as the road and measure it the same way.
  • There is no road yet. If there is no template and no shared pipeline, start there. The dev command is most valuable as part of a road that already exists, and least valuable as a standalone tool nobody was asked for.

One guardrail for the other direction. Paving the inner loop is not the same as mandating an editor, an operating system or a cloud development environment for everyone. Standardise the contract - the dev command, the toolchain file, the dependency decisions - and leave people’s hands alone. The fastest way to lose senior engineers to quiet workarounds is to tell them which IDE to use.

The bottom line

Platform teams pave the part of the developer’s day they can see, and in doing so they leave the part where developers actually spend their hours to a README that rots on contact. The cost is spread thinly across everyone - three days here, a lost afternoon there, a cross-team fix nobody bothered to make - so it never shows up as a line item, and the pipeline quietly becomes everyone’s development environment because local stopped working years ago.

So move the start of the road. One command from a clean machine to a running service, shipped in the template and backed by a shared tool. A written decision for every dependency. Credentials from identity, seed data that exists, and a nightly clean-machine run that fails loudly. Then measure clone-to-running for a stranger, because the stranger is everyone, eventually.

The question I would ask any platform team: how long would it take one of your own engineers to get your busiest service running from scratch, using only what is written down? If nobody on the team wants to find out, that is the next road to pave.

If your developers spend their first week fighting local setup, or the pipeline has become the only place anyone can run their code, that is exactly the kind of problem I help teams work through. Let’s talk.