Skip to content

Composable Architecture

I've been building a new software program, and it's led me to an interesting realisation about composability in software design—not just at the service level, but at the language level too. Different programming languages serve different trade-offs, and they can be composed together beautifully when you define clear boundaries between components.

Take Golang, for instance. What I've noticed is that it's incredibly good at being a reliable workhorse. When you run a Go program, it probably won't break, thanks to its explicit error handling. But here's the thing: in the normal flow of software development, not every component requires that level of bulletproof reliability. While Golang certainly gets the job done, it can require significant effort for simpler tasks where that robustness isn't critical.

This is where TypeScript shines as another composable piece. For common software processes, TypeScript is fantastic—mainly because its community has already built incredibly reliable abstractions that just work out of the box. Each language becomes a building block that you can slot into the right place in your architecture.

A perfect example is database schema management and migrations. Most of Golang approaches are imperative, requiring you to track the entire history of how schemas have evolved. This quickly becomes a maintenance nightmare. TypeScript's declarative schema patterns have become such a standard that tools like Prisma and Drizzle have excellent built-in support, making them perfect components for this part of the system.

The Power of Composable Boundaries

What I've discovered is that the real power comes from treating languages as composable modules rather than monolithic choices. I've composed a system where each language handles what it does best:

  • TypeScript serves as the component for database migrations, GraphQL schema generation, and light application logic
  • Golang acts as the component for mutations—the critical pieces where you absolutely don't want things to fail

Each has a clear interface and responsibility. The boundaries between them aren't arbitrary—they're designed around the natural strengths of each tool.

What's interesting is that this composability extends beyond languages. I'm also implementing an adapter pattern for common services—essentially making infrastructure choices composable too. Right now, everything runs on Postgres, but each adapter is a swappable component: caching could move to Redis, queuing could shift to SQS, and so on.

The magic here isn't just in using multiple languages—it's in creating a system where each component can be optimized independently. Just as Unix tools can be piped together, these language and service choices can be composed to create systems that are both productive to develop and reliable in production.

I keep wondering if there are other languages that might fit as components in this architecture, but TypeScript and Golang seem to cover most of my use cases beautifully. Maybe there's a place for Rust when I need bare-metal performance, or Python for data processing pipelines. The composable approach means I can experiment with these additions without rewriting everything.