angelocatalani / hexagonal_architecture

Notes and proof of concepts about the Hexagonal Architecture in Rust
3 stars 0 forks source link

Congratulations and question #15

Open frederikhors opened 1 year ago

frederikhors commented 1 year ago

Congratulations for your project.

Can I ask you how to deal with db transactions calls in the hexagonal architecture?

angelocatalani commented 1 year ago

Thank you, that's an interesting question I also had.

To simplify the problem let's assume that:

This means that we may have the ARepositoryPort and BRepositoryPort that needs to be invoked by the XService transactionally:

XService<ARepositoryPort, BRepositoryPort>:
    run():
       // how to execute them transactionally ??
       self.a_repo.get_a()
       self.b_repo.get_b()

During my working experience I have never seen a perfectly clean solution to this problem. (Once you know the rules, you can break them). This means that a pragmatic approach will inject the transaction (e.g., the concrete sqlx transaction) as parameter in all the functions that needs to be executed transactionally. We are breaking the rules, the service cannot be unit-tested anymore because it has the database dependency but we can mitigate this with end to end tests.

XService<ARepositoryPort, BRepositoryPort>:
   // we break the dependency rule in the service and consequently in the ports
    run(sqlx_transaction):
       trx = sqlx_transaction.begin() 
       self.a_repo.get_a(trx)
       self.b_repo.get_b(trx)
       trx.commit()

Now, conceptually I think that the cleanest solution is to introduce the ports: TransactionManager and TransactionManagerConnection:

XService<TransactionManager, ARepositoryPort, BRepositoryPort>:
    run():
       trx = self.transaction_manager.begin() // trx is a `TransactionManagerConnection`
       self.a_repo.get_a(trx)
       self.b_repo.get_b(trx)
       trx.commit() 

In this case the ARepositoryPort and BRepositoryPort have both a dependency on another port that is the TransactionManagerConnection.

What do you think? Were you thinking about something different ?

I think a similar pattern can be used to run multiple services transactionally.

I have not implemented yet this approach but I could be doing it in the weekend 👨‍💻

frederikhors commented 1 year ago

This is exactly the issue I'm trying to fix. You got it right.

I'm writing a big document to better represent the issue in a more real-world example.

I'll write you here as soon as I finish it (a few hours I think).

In the meantime, I'll leave you some links that inspired me but unfortunately still haven't helped me solve (I've just started with Rust and I come from languages in which everything is possible and in fact you pay this freedom only later, when the project increase and doesn't scale well).

I hope this can inspire you.

In the meantime thank you.

frederikhors commented 1 year ago

UPDATE: added link for sniper project website.

angelocatalani commented 1 year ago

perfect and yeah I forsee a fiercy buttle with lifetimes and ownership

frederikhors commented 1 year ago

I created https://github.com/frederikhors/rust-clean-architecture-with-db-transactions. I'm still writing the Readme. But you can get the point if you look in main.rs.

InMemory and Postgres repository.

And in services no possibility to use DB transactions.

I'll write also the workarounds I found for this.

frederikhors commented 1 year ago

Here we are: https://github.com/frederikhors/rust-clean-architecture-with-db-transactions/blob/main/README.md