Why We'll Write Every Feature Atleast Twice

TL;DR
- We build every feature twice: TypeScript first, then Go
- TypeScript = rapid iteration + community contributions
- Go = 4-6x performance for proven features
- Not technical debt—deliberate strategy for development speed initially and efficiency in production
Context
We build React hooks that magically turn your boring single-user app into a real-time collaborative experience. We do this by syncing data in real-time between your clients. Behind the scenes, we're syncing state between clients using websockets and maintaining an open-source server that handles all the messy distributed systems stuff.
In less fancy terms: we're a message broker shuffling thousands of messages per second, desperately trying to squeeze every penny of value from our cloud bills. But we're also an early-stage startup that needs to ship features fast and validate ideas even faster.
The cursor that broke the camel's back
The mouse pointer thingy, not the IDE / code editor.
Let me start with an embarrassing confession: moving your cursor continuously in our early prototype could eat up 0.1 vCPU on Railway. For a real-time syncing service, that's not just performance debt, it's basically an existential crisis.
But, we needed that janky prototype. We're building something unproven; something users might hate; something that might be completely wrong. We can't afford to spend months optimizing code that might get thrown away next week.
So the solution we came up with is: we'll build every feature twice.
Do it like a two-pass assembler
First pass: Build it in TypeScript and Node.js. Get it working, get feedback, learn where the edge cases are and iterate fast.
Second pass: When the feature proves itself, we rewrite it in Go for production.
This isn't technical debt masquerading as strategy. This isn't "move fast and break things" cargo cult nonsense. This is a deliberate choice about when to optimize.
Why TypeScript-first makes sense
-
JavaScript programmers are everywhere, and as a open-source project we need these contributors. Stack Overflow's 2024 survey confirms what we already knew; JS is the most popular language by a wide margin. When we're building experimental features, we want community contributions. More contributors means faster iteration and better ideas.
-
TypeScript is expressive. The type system and functional programming support mean less gets lost in translation from idea to code and back to idea again. When you're experimenting, expressiveness beats efficiency.
-
The tooling just works. tRPC gave us websocket multiplexing and client-server type sync out of the box. Sometimes the familiar tool is the right tool, even if it's not the fast tool.
-
Failed experiments cost less. If a feature bombs with users, we've saved weeks we would have spent optimizing it in Go.
The Go graduation ceremony
Node.js is slow. Let's not pretend otherwise. V8 is impressive engineering, but JavaScript wasn't designed for sustained, high-throughput workloads. When our features earn their keep with real users, they deserve better.
Features that graduate to Go get:
- 4-6x efficiency improvements
- (More) proper memory management
- Better concurrency primitives
- Maximum squeeze from every server dollar
Right now we run a Go server that proxies to Node.js for features that haven't been rewritten yet. New stuff launches fast in TypeScript while battle-tested features run at Go speeds.
Why not Rust?
Honestly? We don't know it well enough. We've heard the async story is complicated. Go gives us good enough performance without the learning curve tax.
The math on building twice
Development velocity ROI: TypeScript features ship roughly 2-3x faster than starting with Go, especially for complex business logic.
Infrastructure cost recovery: The performance gains from Go rewrites pay for the engineering time pretty quickly. When you're squeezing every dollar from your compute budget, this matters.
Risk mitigation: This approach reduces technical debt rather than creating it. We're not prematurely optimizing (which might be wrong) or permanently accepting poor performance (which definitely is wrong).
When features earn their rewrite
Not everything needs Go. Here's our framework:
- Procedures & routes for a feature show up consistently towards the top when monitoring
- The feature has stable APIs (unchanged for 2-3 months)
- Gets heavy concurrent usage
- Costs more to run in Node.js than to rewrite
Pretty simple, really.
The honest truth
Everything in this post might be due to skill issues on our part. Some of our architecture exists for legacy reasons, not optimal design. We're still figuring things out.
But that's exactly why this strategy works. We're honest about our constraints: early stage, limited resources, unpredictable which features will succeed. This dual approach acknowledges reality instead of pretending it doesn't exist.
What we're really optimizing for
Different phases need different optimizations:
- Early stage: Learning speed, not runtime speed
- Growth stage: User-critical constraints
- Scale stage: Unit economics
The software industry's obsession with "choosing the right tool" ignores this reality. Sometimes the right choice is refusing to choose (at least not permanently).
At AirState, we need rapid iteration and extreme performance. Rather than compromise on either, we get both. Just not at the same time.
We're building real-time sync infrastructure that doesn't suck. If this approach makes sense to you, we'd love to talk.