Switch DB implementation details at runtime using the repository pattern in a Clean architecture project

This is just an example of an (still incomplete) real-world project written in Rust using a clean architecture: GitHub - frederikhors/rust-clean-architecture-with-db-transactions.

Goals

My intent is to have an app build in 4 layers:

  • entities:

    • some call this layer "domain", not important for now, just the minimum
  • services:

    • some call this layer "use cases", this is where business logic lives (just CRUD methods for now)
  • repositories:

    • some call this layer "adapters", this is where concrete implementation of DB/cache/mail drivers lives
  • ports:

    • some call this layer "controllers or presenters", still not present and not important for now, I'm using main.rs for this

Reproduction

The issue

If you open the main.rs file you can see the issue:

// This obviously works if alone:
// let db_repo = Arc::new(repositories::in_memory::Repo::new());

// This obviously works if alone:
// let pg_pool = Arc::new(sqlx::PgPool::connect("postgres://postgres:postgres@localhost:5432/postgres").await.unwrap());
// let db_repo = Arc::new(repositories::postgres::Repo::new(pg_pool));

// This doesn't work instead:
let db_repo = if use_postgres {
    let pg_pool = Arc::new(sqlx::PgPool::connect("postgres://postgres:postgres@localhost:5432/postgres").await.unwrap());

    Arc::new(repositories::postgres::Repo::new(pg_pool))
} else {
    Arc::new(repositories::in_memory::Repo::new())
};

My intent here is to change repository in use based on a variable, but Rust doesn't like it, this is the error:

Expand the error
error[E0308]: `if` and `else` have incompatible types
--> src\main.rs:37:9
|
28 |       let db_repo = if use_postgres {
|  ___________________-
29 | |         let pg_pool = Arc::new(
30 | |             sqlx::PgPool::connect("postgres://postgres:postgres@localhost:5432/postgres")
31 | |                 .await
...  |
35 | |         Arc::new(repositories::postgres::Repo::new(pg_pool))
| |         ---------------------------------------------------- expected because of this
36 | |     } else {
37 | |         Arc::new(repositories::in_memory::Repo::new())
| |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `repositories::postgres::Repo`, found struct `in_memory::Repo`
38 | |     };
| |_____- `if` and `else` have incompatible types
|
= note: struct `in_memory::Repo` and struct `repositories::postgres::Repo` have similar names, but are actually distinct types
note: struct `in_memory::Repo` is defined in module `crate::repositories::in_memory` of the current crate
--> src\repositories\in_memory\mod.rs:6:1
|
6  | pub struct Repo {
| ^^^^^^^^^^^^^^^
note: struct `repositories::postgres::Repo` is defined in module `crate::repositories::postgres` of the current crate
--> src\repositories\postgres\mod.rs:6:1
|
6  | pub struct Repo {
| ^^^^^^^^^^^^^^^

How can I fix this?

I would prefer a static dispatch method.

Given that requirement, what you have to do is invoke explicitly generic code:

#[tokio::main]
async fn main() -> Result<(), String> {
    if use_postgres {
        let pg_pool = Arc::new(sqlx::PgPool::connect("postgres://postgres:postgres@localhost:5432/postgres").await.unwrap());

        main_part_2(repositories::postgres::Repo::new(pg_pool)).await
    } else {
        main_part_2(repositories::in_memory::Repo::new()).await
    }
}

async fn main_part_2<R: Repository>(db_repo: R) -> Result<(), String> {
    let db_repo = Arc::new(db_repo);
    // ... rest of the code goes here.
}

(Note that this will require the compiler to generate entirely separate machine code for the two cases, resulting in slower compilation and a bigger executable. That's the cost of static dispatch.)

2 Likes

And to avoid those costs?

The dynamic dispatch? How?

(thank you very much)

For dynamic dispatch you would use dyn:

let db_repo: Arc<dyn Repository> = if use_postgres {
    // rest of the code same as your first attempt
    ...

Both of these techniques require you to have a Repository trait that describes the common interface between the two options, which you don't seem to have created yet.

1 Like

Yeah. I have two of them and a supertrait:

And I have the respective inmemory and postgres repositories.

I tried with dyn RepoTrait but there are many error.

I have created a reproduction here: CodeSandbox - CodeSandbox

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.