← Back to list

Unison: The Language That Makes Microservices Feel Like a Monolith

How Content-Addressed Code and Adaptive Service Graph Compression Are Rewriting the Rules of Distributed Systems

Thomas Alexandre · 2026-02-26 10:26 · 5 claps · 6.0 min read
#microservice-architecture #functional-programming #unison #programming-languages
Open on Medium ↗
Wiki topics: 💻 · Programming 📰 · Journalism & News 🏛️ · Architecture

Unison: The Language That Makes Microservices Feel Like a Monolith

How Content-Addressed Code and Adaptive Service Graph Compression Are Rewriting the Rules of Distributed Systems

The Great Migration: From Monoliths to Microservices

In 2011, Netflix completed one of the most ambitious migrations in software history: transforming their monolithic DVD-era architecture into a constellation of hundreds of microservices. Amazon, Uber, and Spotify followed. The promise was compelling — independent deployments, technology diversity, team autonomy, and the ability to scale individual components based on demand.

The monolith’s problems were real. A single codebase meant coordinated deployments, where one team’s change could break another’s feature. Scaling meant scaling everything, even if only the payment service was under load. Technology choices were frozen in time — you couldn’t adopt a better database for just one component.

Microservices solved these problems. But they introduced new ones.

The Hidden Tax of Distributed Systems

Here’s what the conference talks didn’t emphasize: microservices transform compile-time errors into runtime failures.

When you split a monolith, you replace function calls with network requests. That simple orderService.processOrder(cart) becomes:

  1. Serialize the cart to JSON
  2. Make an HTTP request to the order service
  3. Handle network timeouts, retries, and circuit breakers
  4. Deserialize the response
  5. Handle version mismatches when the order service’s API changes

Each service boundary requires serialization code, API versioning, service discovery, and failure handling. Teams spend more time writing infrastructure glue than business logic.

The Kafka Compromise

To avoid the fragility of direct service-to-service calls, the industry adopted event-driven architectures. Instead of the inventory service calling the shipping service directly, it publishes an “OrderPlaced” event to Kafka. The shipping service subscribes and reacts.

This decoupling has real benefits — services can fail independently, and the event log provides a durable record. But it comes with costs:

  • Eventual consistency: You can’t get an immediate response. The UI might show “Order Placed” while the shipping service hasn’t even seen the event yet.
  • Debugging complexity: Following a request through multiple services requires distributed tracing infrastructure (Jaeger, Zipkin).
  • Schema evolution pain: Changing an event’s structure requires careful versioning. Old consumers must handle new fields; new consumers must handle old events.
  • Operational overhead: Kafka itself becomes critical infrastructure requiring expertise to operate — partition management, consumer group coordination, retention policies.

The industry accepted this complexity as the price of scalability. Kubernetes, Istio, and service meshes emerged to manage it. DevOps became a discipline unto itself.

Then Unison asked: what if the assumptions were wrong?

The Unison Revolution: Content-Addressed Code

Unison starts from a radical premise: what if code were identified by its content, not its name?

Every function in Unison is stored as a hash of its abstract syntax tree. The function List.map isn't identified by that string—it's identified by something like #abc123def456. The name is just metadata, a human-readable pointer to that hash.

This seemingly simple change unlocks remarkable capabilities:

No builds, ever. When you define a function, it’s parsed and typechecked exactly once, then stored permanently. There’s no “rebuilding” because the definition at hash #abc123 never changes.

No dependency hell. The “diamond dependency problem” — where two libraries require different versions of a third — simply doesn’t exist. Each version has a different hash. They coexist peacefully. You upgrade when you’re ready.

Code as data. A function’s hash can be stored in a database, sent over a network, or passed as a value. The runtime can fetch any missing dependencies on demand.

Adaptive Service Graph Compression: The Game Changer

Here’s where it gets interesting for microservices.

In traditional architectures, if Service A calls Service B which calls Service C, you have three separate processes, potentially on three separate machines, with two network round-trips. The latency adds up. The failure modes multiply.

Unison’s Adaptive Service Graph Compression takes a different approach. Because code is content-addressed and can be transferred between nodes, the runtime can dynamically relocate computations based on actual usage patterns.

If Service A frequently calls Service B, the runtime can co-locate them — running B’s code within A’s process when it makes sense. The call that looked like a network request becomes a function call again. The serialization disappears. The latency vanishes.

But here’s the key: you still write services as if they were separate. You get the organizational benefits of microservices — independent deployments, clear boundaries, team autonomy — without the performance penalties.

The system observes the call graph at runtime and compresses it. Services that rarely interact stay separate. Services that constantly communicate merge dynamically. The topology adapts to actual load patterns, not to the architecture diagram you drew six months ago.

What This Looks Like in Practice

In traditional microservices, calling another service requires infrastructure:

# Traditional approach
import requests
import json
def get_user_orders(user_id):
    response = requests.get(
        f"http://order-service/users/{user_id}/orders",
        headers={"Authorization": get_service_token()},
        timeout=5
    )
    response.raise_for_status()
    return OrderListSchema().load(response.json())

In Unison, it’s a function call with type safety:

getUserOrders : UserId ->{Remote} [Order]
getUserOrders userId = Services.call orderService userId

That’s it. No serialization code. No retry logic. No service discovery. The {Remote} ability in the type signature indicates this is a distributed call, but the compiler ensures type compatibility between caller and callee. If the orderService changes its return type, your code won't typecheck.

Deploying a service is equally simple:

cloud.services.deploy getUserOrders

No Dockerfile. No Kubernetes YAML. No Helm charts. The runtime handles distribution, and because deployments are content-addressed, they’re automatically immutable and idempotent.

Beyond Microservices: Unison’s Other Innovations

The content-addressed foundation enables several other features that address long-standing pain points:

Typed Durable Storage

Storing data typically requires defining schemas, writing serialization code, and handling schema migrations. In Unison, you can persist any value — including functions — directly:

Storage.put "myKey" someComplexValue

The value’s type is preserved. When you retrieve it, you get back exactly what you stored, with full type safety. No ORM. No schema files. The hash ensures the data remains interpretable even as your codebase evolves.

Fearless Refactoring

Renaming a function in a traditional codebase requires updating every call site, hoping you didn’t miss one, and dealing with version compatibility. In Unison, renaming is instantaneous and non-breaking — you’re just updating the human-readable name that points to an unchanged hash.

Similarly, moving code between modules has zero risk. The underlying hashes don’t change, so nothing breaks.

Algebraic Effects for Clean Architecture

Unison’s ability system (algebraic effects) lets you write code that’s explicit about its effects without the complexity of monad transformers:

processOrder : Order ->{Database, Payment, Log} Receipt

The type signature declares exactly what this function can do — access a database, process payments, and write logs. In tests, you can provide mock handlers. In production, you provide real ones. The business logic remains unchanged.

This is dependency injection done right, checked by the compiler.

Incremental Everything

Because parsing and typechecking results are cached by hash, Unison achieves true incrementality. Change one function, and only that function gets rechecked. The rest of your million-line codebase? Already done. The compiler isn’t racing to process megabytes of code — it processed them once, months ago, and never will again.

The Philosophical Shift

Unison represents a different philosophy about what a programming language should be.

Traditional languages are text-processing systems. Your code is a pile of files. The compiler reads them, does work, and produces artifacts. Every build starts from text.

Unison treats code as a structured database. Definitions are stored as typed syntax trees, identified by content hashes. The “codebase” is a navigable graph of definitions, not a directory of files. Names are metadata. The source of truth is the hash.

This shift eliminates entire categories of problems:

  • No more “works on my machine” (same hash, same behavior)
  • No more coordinated deployments (each service is independently addressable)
  • No more breaking changes (old hashes continue to work)
  • No more lost code (everything is content-addressed and cacheable)

When Should You Consider Unison?

Unison shines for:

  • Distributed systems where service-to-service communication is common
  • Teams tired of infrastructure complexity who want to focus on business logic
  • Organizations drowning in YAML for deployment configurations
  • Projects requiring strong correctness guarantees with typed effects

It may not be the right choice if:

  • You need extensive library ecosystems (Unison is young)
  • Your team is heavily invested in existing tooling
  • You require specific platform integrations not yet available

The Future of Distributed Computing

For two decades, we’ve accepted that distributed systems are fundamentally different from local programs. We built elaborate infrastructure — service meshes, message queues, container orchestration — to manage that difference.

Unison suggests another path: what if distributed computing felt like local computing? What if the runtime handled location, serialization, and communication transparently? What if you wrote business logic while the platform optimized the topology?

Adaptive Service Graph Compression isn’t just an optimization — it’s a statement that the boundary between monolith and microservices is artificial. With the right foundation, you can have the organizational benefits of services and the performance characteristics of a monolith, dynamically adjusted based on actual usage.

The teams building Unison come from serious distributed systems backgrounds. They’ve experienced the pain they’re solving. And with the recent 1.0 release, the language has reached a stability milestone.

The microservices revolution solved real problems. But it created new ones. Unison isn’t asking us to go back to monoliths. It’s asking: what if we could move forward to something better?

Sources


메타데이터
post_id
64fa9cc5072c
slug
unison-the-language-that-makes-microservices-feel-like-a-monolith-64fa9cc5072c
url
https://medium.com/@thomas_alexandre/unison-the-language-that-makes-microservices-feel-like-a-monolith-64fa9cc5072c
canonical_url
https://medium.com/@thomas_alexandre/unison-the-language-that-makes-microservices-feel-like-a-monolith-64fa9cc5072c
author_url
https://medium.com/@thomas_alexandre
status
ok
fetched_at
2026-07-15 06:54:52