Open frederikhors opened 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 👨💻
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).
https://github.com/skerkour/bloom-legacy/tree/v3/bloom (the explanation is here: https://kerkour.com/rust-web-application-clean-architecture/), my issue to ask this: https://github.com/skerkour/bloom-legacy/issues/70;
https://github.com/dpc/sniper: in particular I would like you to notice this file: https://github.com/dpc/sniper/blob/master/src/persistence.rs; he's trying to do the same, but it's too difficult for me to understand all the code yet. You can read about it here: https://dpc.pw/data-oriented-cleanandhexagonal-architecture-software-in-rust-through-an-example.
I hope this can inspire you.
In the meantime thank you.
UPDATE: added link for sniper project website.
perfect and yeah I forsee a fiercy buttle with lifetimes and ownership
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.
Congratulations for your project.
Can I ask you how to deal with db transactions calls in the hexagonal architecture?