Every founding engineer picking the tools they already know is the correct decision. The cost shows up later, in auditors and headcount, and by then nobody can name who made it.
Picture the first year of a startup that is actually working. Three or four engineers, product-market fit still a moving target, and a rule that everybody agrees with without ever saying it out loud: ship the thing. Microservices, because that is what you do, and because it means nobody has to wait on anybody else to merge.
Engineer A likes Python. FastAPI, a REST API, transaction script all the way down, because the service is four endpoints and a database and layering it would be a joke. Ships it on Kubernetes. Done, on time, and the client team can call it.
Engineer B loves Go and has opinions. Standard library, no framework, and he has read Uncle Bob so the service comes out as clean architecture with the interfaces pointing inward. He thinks Kubernetes is a lot of machinery to run one binary that could sit on a VM, so it sits on a VM. Ansible does the setup. Also done, also on time.
Engineer C has never written a line of either language. He came from enterprise Java where everything was a Lambda, so he writes Lambdas. His code structure is whatever the handler signature wants it to be, with a bit of ports and adapters where it happened to fit. Deployed to AWS Lambda. Working, in prod, delivering value.
Nobody did anything wrong. That is the part the usual telling gets wrong, because it treats those three as villains and they are not. Each one used the fastest path they personally had to working software, at a stage where being slow is the only failure mode that actually kills you. Force all three onto one stack in month two and you pay for it in weeks of ramp-up during the exact window when weeks matter most.
Then it works. That is the whole problem.
the bill is not the code
The startup grows. You go from four engineers to fourteen to forty, and now the three founding engineers are not writing the services anymore, they are running teams that own them. And you hit the thing that nobody put on a risk register.
Team A is drowning. Team B has slack. You want to move one person for a quarter.
You can’t. Not really. The Go engineer knows Go, clean architecture, and how the Ansible playbooks are laid out. Team A is Python, transaction script, Kubernetes manifests, and a deployment story that shares zero vocabulary with the one he knows. You are not moving an engineer across a team boundary, you are onboarding a new hire who happens to already be on payroll. Two months, optimistically, and you burn team A’s review capacity teaching him while team A is the team that was already underwater.
This is the failure mode I keep seeing, and it never announces itself as a technology problem. It shows up in a staffing conversation. Somebody asks why we can’t just move people around, and the honest answer is that there is no „around,“ there are three separate companies with a shared Slack.
But engineer mobility is the cheap part of the bill. Here is the expensive part.
every stack pays for its own compliance
Somebody eventually asks for SOC 2. Or a customer sends a security questionnaire with three hundred rows. Or you sell into a regulated industry and now there is an auditor.
The auditor does not ask „how does the company handle production access.“ The auditor asks how this system handles production access, and then asks again about the next one, because they are different systems.
So for the Kubernetes services: how does someone get a shell, who approved it, where is that logged, what does RBAC look like, how do secrets get in, how do you prove the running image is the one that came out of CI. Fine. Real work, but it is one body of work and it covers every service on that cluster.
Now do the VMs. SSH access is a different question from kubectl exec. Different audit log, different place it lands, different way to prove nobody bypassed it, different answer for how you rotate credentials, different story for how a config change gets reviewed before it hits prod. None of the Kubernetes answers transfer. You are writing the second copy of the same document with none of the same words.
Now do Lambda. IAM roles, execution context, CloudTrail, and its own model for who can invoke what and how you would even notice if someone changed a function outside of the pipeline.
Three stacks means three answers to every single control. Three sets of runbooks. Three implementations of audit logging. Three ways to prove an employee cannot log into prod and break something quietly, and three places to enforce a second factor when someone does need to fix a live incident at 2am. And the writing is the small half of it. It is three of everything, forever, re-verified every renewal cycle, by a security team that is probably two people.
That is the invoice. It arrives in year three, addressed to nobody, for a decision three people made independently in year one while doing exactly what the company needed them to do.
so pick, and pick less than you want to
The fix is not exotic. It is deciding on purpose, earlier than feels necessary, and then holding the line when it is mildly inconvenient.
Start with languages. One frontend language, one backend language, is the target you should have to argue your way out of, not argue your way into. Two backend languages is defensible when the domains genuinely diverge. Python for anything ML or data heavy is a completely reasonable second, because fighting the ecosystem there costs more than the extra stack does. Go for the services that are mostly network and concurrency, also reasonable. What is not reasonable is a fourth language because somebody wanted to try it, or a third because a contractor was comfortable with it.
The test is not „is this language good.“ Every language on the shortlist is good. The test is whether you can move a person onto it in a week, and whether the security work you already did applies to it.
Then deployment, and this is where I would be strictest, because the compliance cost concentrates here. One runtime. Not one per team, one. Observability works the same way. Pick the logging, metrics, and tracing stack once, and make it the thing a new service gets by default rather than a thing each team assembles. When it is one platform, you write the access model once, the audit logging once, the incident procedure once, and every team inherits it whether they thought about it or not. The good outcome is that a team can ship a service without ever having to make a security decision, because the decisions already sit in the path of least resistance.
Doing this at four engineers feels like premature process. It is not. It is the cheapest hour you will ever spend, because it costs you a conversation now and saves you a multi-year duplicated compliance program later. Doing it at forty engineers is a migration with a political dimension, which is a nice way of saying it mostly does not happen.
what this looks like when you commit
I run this hard on my own stuff, partly because I wanted to feel the tradeoff myself rather than just recommend it.
Frontend is TypeScript, all of it. React on web, React Native and Expo on mobile, because I want web and native apps out of one set of skills and one set of components. Backend is Rust, and the honest reason is the type system. When I want an abstract interface I define a trait, and the compiler makes sure every implementation actually holds up its end. That has saved me a category of late-night surprise that I used to just accept as normal.
Deployment is one open-source Kubernetes setup and nothing else. K3d for the control plane, Grafana with Loki, Tempo, and Mimir for logs, traces, and metrics. Flux as the GitOps controller, so what is in the repo is what is running. Flagger for canary deploys. There is exactly one way an app of mine reaches production, and I never have to think about it again after the first time.
The piece that ties it together is a CLI, shared library, and template engine I built called Baukit. It scaffolds a new app so the boring parts are already there and already look like every other app I have. The real value is the loop. When I solve something properly in one app, I push the solution back into Baukit, and the next two apps get it without me reimplementing anything. My agents drive the CLI directly, which means „start a new app“ is a command rather than an afternoon.
Fair warning, it is on GitHub in alpha. I am not taking pull requests yet and I will break things. I am pointing at it as an example of the shape, not as something you should adopt tomorrow.
The result is that every app I ship looks and feels like the last one, every lesson compounds instead of being relearned, and I ship faster now than I ever have. Not because Rust is fast or Kubernetes is clever. Because I stopped re-deciding.
the actual lesson
You are not going to avoid this by hiring people with better judgment. Those three engineers had good judgment. They optimized for the thing that was correctly the most important thing at the time, and the cost of their choice was invisible for two years and then landed on somebody else’s desk.
So the job is to make the decision at a moment when it is still nearly free, and to be honest that you are trading a small amount of speed today against a much larger amount of drag later. Pick your languages. Pick one way to deploy. Build the paved road, even a rough one, and make it the easiest path so nobody has to be talked into it.
Then get back to shipping. That was always the point.