Rust Microservices - Project Structure
By Nikita Bishonen • 7 minutes read •
Hi! Today I’m sharing my thoughts on a convenient project structure for developing microservices in Rust. This is the first post in a potential “Rust Microservices” series. If I manage to finish it, I’ll compile it into a full book, so if you notice any inaccuracies or have your own thoughts on the topic - I’d be happy if you share them with me.
I’ll start this post with a brief overview. If you’re confident in your knowledge of Rust’s package and module system, feel free to jump to the core concept.
Packages, Crates, and Modules in Rust
If you’re not familiar with Rust, I recommend studying the original source - the chapter Managing Growing Projects with Packages, Crates and Module in the official Rust documentation. What follows is my interpretation, which may be inaccurate or incomplete.
So, in Rust projects we have:
- A Package containing:
Cargo(.toml | .lock)- One or many crates, each having:
- A crate root -
src/(lib.rs | main.rs | bin/*.rs) - Modules
- inline -
mod _name_ { _body } - as separate files -
mod _name_;+src/(_name_.rs | _name_/mod.rs)
- inline -
- A crate root -
This creates a nesting doll or the tale of Koschei the Deathless - the needle (module) in an egg, the egg (crate) in a chicken, the chicken (package) in a chest (project).

Let’s examine the rules for accessing modules.
- Modules can be private (hidden) and public.
- By default, modules are private.
- Entities inside a module can also be private and public.
- By default, entities are private.
- And then interesting interactions and combinations begin, which I propose to examine with code examples.
mod parent {
mod children_private {
struct Private;
pub struct Public;
}
pub mod children_public {
struct Private;
pub struct Public;
mod grandchildren {
struct Private;
pub struct Public;
}
}
}
mod neighbour {
}Visually, the structure looks like this:

With this example, I want to show you the possible connections between modules and entities (in this example, we limit ourselves to empty structs).
Let’s break this down into several situations, which we’ll distinguish by the calling side.
What Can a Parent Module Access?
fn test_visibility() {
// children_private::Private; // Fails due to privacy of struct
children_private::Public;
// children_public::Private; // Fails due to privacy of struct
children_public::Public;
// children_public::grandchildren::Private; // Fails due to privacy of module.
// children_public::grandchildren::Public; // Fails due to privacy of module.
}Above, I show a test snippet for the parent module. As we can see, access is available to both (private and public) child modules, but only to public structs. Access to private “grandchildren” and their public structs is not available. Visually, this looks like:

What Can a Child Module Access?
For this, we’ll make an addition and add two structs to the parent module, analogous to the children.
/// Test visibility of super model and another sub-models of module one level above.
fn test_visibility() {
super::Public;
super::Private;
// super::children_private::Private; // Fails due to privacy of struct
super::children_private::Public;
// self::grandchildren::Private; // Fails due to privacy of struct
self::grandchildren::Public;
}
As we can see, our child’s tentacles reach quite far, and only private structs of sibling and child modules are inaccessible. However, private structs of the parent module are well within reach.
What Can a Great-Grandchild Access?
/// Test visibility of super model and another sub-models of module two levels above.
fn test_visibility() {
super::Private;
super::Public;
// super::super::children_private::Private; // Fails due to privacy of struct
super::super::children_private::Public;
super::super::Private;
super::super::Public;
}
For the great-grandchild, the situation is similar. I decided to show it for better understanding of how this multi-level mechanism works in Rust.
What Can a Neighbor Access?
Unfortunately, I only noticed at this stage that all diagrams up to this point included the neighbor inside the parent module. In reality, it’s located alongside, not inside the parent. The diagram below contains the correct placement of this module.
/// Test visibilit of sub-modules inside another module on the same level.
fn test_visibility() {
crate::parent::Public;
// crate::parent::Private; // Fails due to privacy of struct
// crate::parent::children_private::Private; // Fails due to privacy of module
// crate::parent::children_private::Public; // Fails due to privacy of module
// crate::parent::children_public::Private; // Fails due to privacy of struct
crate::parent::children_public::Public;
// crate::parent::children_public::grandchildren::Private; // Fails due to privacy of a module
// crate::parent::children_public::grandchildren::Public; // Fails due to privacy of a module
}
The neighbor’s situation is quite boring - only public entities and paths to them are accessible. Note that the parent module is not public, yet the neighbor can still see it 👀
SOVMS
Or Structure Based on Visibility of Modules. The idea is that in Rust, we already have defined access rules (which I described in detail above) to module entities depending on their relationships (what relationship the calling module has to the called module).
I decided that the project structure should also be laid out based on these Rust language features to be more idiomatic. The idea is new, and as I test it, I’ll update this post.
To get an idiomatic project structure, we should place modules based on who and what should see them. Is this part of the code used throughout the entire service? Move it as high as possible. Is another part of the code specific? Isolate it in a separate branch or hide it deeper.
The point is that modules shouldn’t be decomposed according to business domains (at least this shouldn’t be the basis of division). After all, in microservice architecture, these should be separate services (or separate Rust packages for monolithic applications). This way, we strive for the coveted golden ratio of loose coupling and high cohesion.
TL;DR
How do I see using this approach in writing microservices? Let me not waste more of your time reading my thoughts and show you a template:
//! This is the base service crate. Better to make it a library, so functionality regarding how
//! exactly to run it will be stored separately inside one main.rs or different bins.
mod interfaces {
//!
mod graphql {
//!
}
mod http {
//!
}
mod grpc {
//!
}
}
mod core {
//!
mod api {
//!
}
mod config {
//!
}
}
mod resources {
//!
mod db {
//!
}
mod kafka {
//!
}
mod third_party_integration {
//!
}
}You can create a new package with two crates:
- main is responsible only for starting the application
- lib stores the structure and is used in main, tests (integration), and additional “bins” if needed
Real Example
In this example, some names have been changed, but this is a real structure of one of several services. Writing them actually prompted me to derive some general theory regarding structuring microservices in Rust.
src
├── bin
│ └── events_processor.rs
├── core
│ ├── api.rs
│ ├── configuration.rs
│ └── mod.rs
├── interfaces
│ ├── graphql
│ │ ├── mod.rs
│ │ ├── mutations.rs
│ │ ├── queries.rs
│ │ └── subscriptions.rs
│ └── mod.rs
├── lib.rs
├── main.rs
└── resources
├── node_proxy.rs
├── indexer_proxy.rs
├── db.rs
├── mod.rs
├── reservoir_proxy.rs
└── real_time_data_ws.rsMy Experience
I can say that I find this structure convenient because it combines the advantages of the good old three-layer architectures and hexagonal architectures applicable to microservices. We divide our application into three branches:
Resources- everything that’s outside our service and where we’re consumers goes here. Databases, other services, message brokers (partially). Everything where we initiate communication and where we know more about the interlocutor than they know about us. (Previously I called this partclients, but the name resources seems more appropriate now). The main task of resources is to handle the specifics of interacting with these resources. Here we can have implementations of traits for encoding and decoding our data types for a specific database, and here we write “proxies” that transform schemas of other services into the one we operate with inside the service.Interfaces- everything that serves as entry points for requests to our service goes here. HTTP/gRPC API, GraphQL, even message brokers if it’s part of processing incoming requests - everything goes here. Just like resources, here we try to isolate all specifics related to the particular mechanism of client-server interaction (that’s why I previously called this partservers). For example, all request deserializations and response serializations should be implemented here. After that, we should work with “clean” types.Core- this is the “cleanest” and “most important” place in our service. The main business logic is implemented here. Here we operate not with JSON that came over the network, but with its statically typed and verified representation. From the core, we can call resources, but ideally should know nothing about interfaces. However, we shouldn’t have access to low-level concepts of resources. Only to the public API.
How does module placement help here? We see that all three branches are neighbors to each other, meaning they can only know public things, and specifics are safe to change (for example, changing the database or supporting REST alongside gRPC).
Inside branches, we go from general to specific and also separate independent parts into branches.
graphql consists of independent queries, mutations, subscriptions, but the common schema is in the root module:
//! graphql/mod.rs
pub async fn schema() -> Result<Schema<Query, Mutation, Subscription>> {
let configuration = Configuration::from_env()?;
let pool = database::connect(&configuration.database).await?;
let producer = kafka::build_producer(&configuration.kafka)?;
Ok(Schema::build(Query, Mutation, Subscription)
.enable_federation()
.enable_subscription_in_federation()
...Each sub-module will call public “clean” methods from core/api.rs.
api.rs in my example contains common types in the root (both private and public), which its sub-modules api::queries and api::commands have full access to, but private types and implementations of the api module itself are inaccessible to anyone except its sub-modules.
Additional Materials
If you’re interested in “playing” with the example from this article, here’s the full project code from the modules chapter:
#![allow(clippy::all, dead_code, path_statements)]
fn main() {
println!("Hello, modules privacy!");
}
mod parent {
struct Private;
pub struct Public;
mod children_private {
struct Private;
pub struct Public;
}
pub mod children_public {
struct Private;
pub struct Public;
mod grandchildren {
struct Private;
pub struct Public;
/// Test visibility of super model and another sub-models of module two levels above.
fn test_visibility() {
super::Private;
super::Public;
// super::super::children_private::Private; // Fails due to privacy of struct
super::super::children_private::Public;
super::super::Private;
super::super::Public;
}
}
/// Test visibility of super model and another sub-models of module one level above.
fn test_visibility() {
super::Public;
super::Private;
// super::children_private::Private; // Fails due to privacy of struct
super::children_private::Public;
// self::grandchildren::Private; // Fails due to privacy of struct
self::grandchildren::Public;
}
}
/// Test visibility of sub-modules inside current module.
fn test_visibility() {
// children_private::Private; // Fails due to privacy of struct
children_private::Public;
// children_public::Private; // Fails due to privacy of struct
children_public::Public;
// children_public::grandchildren::Private; // Fails due to privacy of module.
// children_public::grandchildren::Public; // Fails due to privacy of module.
}
}
mod neighbour {
/// Test visibilit of sub-modules inside another module on the same level.
fn test_visibility() {
crate::parent::Public;
// crate::parent::Private; // Fails due to privacy of struct
// crate::parent::children_private::Private; // Fails due to privacy of module
// crate::parent::children_private::Public; // Fails due to privacy of module
// crate::parent::children_public::Private; // Fails due to privacy of struct
crate::parent::children_public::Public;
// crate::parent::children_public::grandchildren::Private; // Fails due to privacy of a module
// crate::parent::children_public::grandchildren::Public; // Fails due to privacy of a module
}
}