When you build a system where decisions have financial consequences, the question of what runs where is not a preference. It is a guarantee. The signal engine and the API layer are separate processes because they cannot be the same process.
Most web applications are written in one language and deployed as one process. This is the right default. A single language means a single deployment, a single mental model, a single set of debugging tools. For the vast majority of systems — SaaS platforms, marketing sites, internal tools — the tradeoff is correct. You give up a little performance headroom you will never need, and you gain the ability to move fast.
Trading systems are not most web applications.
What the engine does
The signal engine has one job: evaluate whether market conditions warrant a trade. It reads price data from a Kafka topic, runs a sequence of technical indicators — RSI, MACD, volume ratio, VWAP deviation, ATR — and produces a confidence score. If the score clears the threshold, it broadcasts a signal. The whole evaluation happens in microseconds.
The critical constraint is that this evaluation cannot be interrupted. A garbage collection pause of even a few milliseconds can mean the difference between entering at the right price and entering after the move has happened. In paper trading, a missed entry is a statistic. In live trading with real capital, it is a loss.
Go's garbage collector is not magic — it still pauses. But it is tunable, predictable, and fast in a way that a JavaScript runtime's GC is not. More importantly, Go gives you the ability to reason about memory allocation in a way that lets you minimize GC pressure in the first place. The engine allocates as little as possible in the hot path. The result is evaluation latency that stays flat under load rather than spiking unpredictably.
What the API does
The API layer does everything else. User authentication. Session management. Persisting trades to the database. Calling Claude to evaluate news sentiment before a session starts. Serving the React frontend. Sending WebSocket updates to the browser. Managing the watchlist, the configuration, the post-session analysis.
None of this is latency-sensitive. A user loading their session history can wait 200 milliseconds. The pre-market briefing can take three seconds to generate — it involves multiple Claude API calls and is cached anyway. The WebSocket that pushes price updates to the browser runs on a three-second poll cycle. These are human timescales, not market timescales.
TypeScript and Bun are the right tools for this layer. The ecosystem is enormous. The Claude SDK is first-class TypeScript. Prisma handles the database with full type safety. The development speed is materially faster than Go for this kind of work — HTTP routing, middleware, request validation, ORM queries. You write less code and it breaks in more legible ways.
Why the boundary matters
The reason to keep these two layers separate is not performance alone. It is isolation.
When the API layer handles a slow Claude API call — three seconds to generate a market briefing — that slowness is contained. It does not affect signal evaluation. The Go engine does not know the API exists. It reads from Kafka, writes to Kafka, and evaluates signals. If the API process crashes, the engine continues running. If the engine crashes, the API continues serving the frontend.
This independence has operational consequences. You can restart the API without restarting the engine. You can deploy a change to the session management logic without touching anything in the signal evaluation path. You can scale them independently if you need to — though in practice, the engine is so lightweight that this has never been necessary.
The alternative — a single TypeScript process doing everything — would work fine in paper trading. In live trading, the first time a Claude API call blocks for five seconds during active market hours, the signal evaluation queue backs up and you miss entries. The boundary does not prevent this failure mode. It makes it impossible.
The general principle
Not every system needs two languages. Most systems are better off with one. The maintenance overhead of a polyglot architecture is real — two build systems, two test harnesses, two sets of deployment concerns, a communication layer between them.
The question to ask is not "would Go be faster here?" It almost always would be. The question is "does the performance characteristic of this component matter enough to justify the cost of isolating it?" For most components in most systems, the answer is no. For a signal engine in a live trading system, the answer is yes — and the isolation is valuable independently of the performance gain.
Use the right tool for each problem. Keep the boundaries clean. Make sure the boundary serves a purpose you can name, not just a preference you cannot justify.
The engine is not the API because they are doing fundamentally different things under fundamentally different constraints. The language choice follows from that. The separation of processes follows from that. The architecture is not clever. It is just honest about what each part of the system actually needs.