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

506 views Asked by At

This is just an example of an (still incomplete) real-world project written in Rust using a clean architecture: https://github.com/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

https://codesandbox.io/p/github/frederikhors/rust-clean-architecture-with-db-transactions/main

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?

1

There are 1 answers

11
cafce25 On

Like always with different types you have essentialy 2 options, static or dynamic dispatch.

For dynamic dispatch both Repo types have to implement a common trait that encompasses all necessary functionality and is object safe:

pub trait Repo {
    fn do_stuff_with_repo(&self);
    // etc
}

impl Repo for postgres::Repo {
    fn do_stuff_with_repo(&self) {
        //...
    }
}

impl Repo for in_memory::Repo {
    fn do_stuff_with_repo(&self) {
        //...
    }
}

let db_repo: Arc<dyn 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())
};

or for static dispatch create an enum and everytime you have to deal with it you pattern match:

enum Repo {
    Memory(in_memory::Repo),
    Postgres(postgres::Repo),
}
let db_repo = if use_postgres {
    let pg_pool = Arc::new(sqlx::PgPool::connect("postgres://postgres:postgres@localhost:5432/postgres").await.unwrap());

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