Microservices to Monolith, Rebuilding Our Backend in Rust
By
Logan Cox /
Developer
Jun 26, 2025
Navigate to:
Project Members: Dustin Eaton, Joel Abshier, Logan Cox, Mohammad Naqvi, Tanish Garg
The following serves as a practical guide for those looking to simplify their architecture by migrating to a Rust monolith.
Earlier this year, the platform team at InfluxData undertook a major rewrite of our core account and resource management APIs, moving from Go to Rust and from a microservices architecture to a single monolith. This change supported a new administrative UI for InfluxDB Cloud Dedicated and aligns with our broader effort to rewrite the InfluxDB database engine in Rust.
This refactor allowed us to reassess our design patterns and formalize our approach to building backend services in Rust. As part of this effort, we’re also building a new V1 REST API and migrating all of our legacy gRPC and REST endpoints, all while reducing service complexity.
Figure 1: Original service layout, clients routed to a set of microservices via a gateway service, each using their own database.
Figure 2: Desired service layout, a single monolithic service. Using a single database.
Why rebuild?
The decision behind the rewrite was to reduce the overall complexity of the service from an infrastructure and development perspective. As more of our internal services are written in Rust, standardizing on a single language makes it easier for engineers to collaborate across multiple projects without constantly switching between languages and frameworks. A monolith fits our team model as well: one team, one backend service, one language.
There’s also a deployment advantage. These services ship “on-premise” into customer Kubernetes environments, including in air-gapped environments. The simpler the architecture, the easier it is to manage in those constrained environments.
Why Rust?
Rust offers several advantages over Go, particularly in terms of safety and maintainability.
- Stronger type system: Rust reinforces software correctness, discovering more issues at compile time.
- Null safety:
Option<T>
ensures we handle absent values deliberately. - Complete and safe error handling: Go does not force you to check a returned error; Rust uses the
Result<T, E>
type. This further ensures software correctness and proper error handling due to the Result type using the must-use macro. - No implicit defaults: In Rust, you must be explicit with your intentions, which avoids bugs related to assumed default values.
- Explicit Mutability: In Rust, mutation is opt-in and explicit; you are prevented from mutating an immutable value, making it easier to analyze and edit code compared to Go, where variables are mutable by default.
- Macros: Allow us to significantly reduce boilerplate in our backend and focus on core logic.
Our migration approach
Since these services were already in production, we couldn’t afford downtime for any service or endpoint we migrated. We chose a strangler fig migration, which allowed us to migrate endpoints one by one with no downtime or regressions in functionality.
We would first write a Rust proxy layer that calls back to all the Go services and point our ingress to this new service within Kubernetes. Once deployed, this pattern means that our new Rust service handles the routing for every service. In a single PR, we could migrate a single endpoint and replace the proxy functionality for that endpoint to call internal logic. Because our application tier is stateless, we ran the new Rust service alongside the old Go services and there were no adverse effects, as the logic is identical. So, how did we know the logic was identical? We first created a suite of integration tests in the Rust service that would call the Go services. This allowed us to model their functionality, and then when we remove the proxy functionality to call internal logic, those same integration tests should pass. This is what allowed us to move quickly in this project: upfront modeling of our system, replacing pieces one by one, and continuous testing along the way. Here is a representation of that migration, showing more and more logic being shifted to the Rust service until it eventually replaces all of the Go services.
Figure 3: The Strangler Fig Migration approach for rewriting services. Initially (left), the Rust facade service acts as a proxy for the original Go services. Then, endpoint by endpoint, migrate functionality (middle) until, eventually, the new service engulfs the original. The original Go service can now be decommissioned (right).
New API
To add new functionality to our V1 REST API that supports our new management frontend, we coordinated with the frontend team on which pages they would build first. This allowed us to plan the endpoints we needed to have ready for a smooth delivery of the administration UI for cloud dedicated.
A large part of what allowed us to ship a migration of a v0 REST API, gRPC API, and a new v1 REST API as part of the same project, was our design patterns. We knew it was best to use domain-driven design that would allow us to model specific business and logic units in isolation from our infrastructure and enable modularity (A large part of our domain design was to follow the new type pattern and the “parse don’t validate” rule). This modularity also allowed us to implement hexagonal architecture. This means:
- Business logic does not need to care about network protocol or database implementation, these are pluggable elements.
- It is easier to swap out database platforms or API architecture without touching business logic.
- Business logic can be tested independently of the data access functionality, and we can wrap it all in integration tests.
Figure 4: Hexagonal Architecture, adapters, and interfaces connect external pluggable infrastructure components to core business logic. With hexagons representing the boundaries between business logic and external adapters.
What did we learn?
- Upfront design in the migration approach allowed us to ship faster; a team of five delivered this project across three months.
- Some of the team members believe that because of upfront design decisions and the move to Rust, we now ship functionality faster than before.
- We now have safer software due to the move to Rust.
- Good integration tests allowed us to move quickly on both functionality and AuthZ/AuthN validation.