Android-CleanArchitecture
Android-CleanArchitecture copied to clipboard
How to deal with transactions in domain layer
I have a question about handling transactions between data and domain layer. Think about a usecase where two entities should be persisted into a local database in one transaction. Should the domain layer know something about "transactions" when calling into the data layer and what about "nested transaction"? Who is responsible to decide whether two or more operations should handled atomic as the domain layer doesn't know something about where and how the data is persisted. Another point is also how to handle if there is a foreign key relationship between entities.
Thank you for any advice.
Those are all implementation details, and should therefore all be contained in the data layer.
but the data layer is not aware of which operations on data should be handled atomic, i think thats a detail to the usecase and even to the presentation. But i dont know how to provide the domain layer an appropriate interface to the data layer where multiple operations can be handled transactional, including support for relationship between entities.
The data layer should be the only layer aware of transactions with a database, or anything that has to do with getting/persisting data.
In the article describing that data layer... "The idea behind all this is that the data origin is transparent for the client, which does not care if the data is coming from memory, disk or the cloud, the only truth is that the data will arrive and will be got."
The presentation layer displays data when it is provided by a UseCase. The domain layer implements the UseCases which describe the business logic of the application. The UseCases should not require any knowledge of the data layer.
The repository pattern is what abstracts the need for transactions etc. away from the domain layer, and allows the data layer to handle those implementation details.
I already read the article , but i think i couldn't describe my problem in a clear way. As i understood the domain layer should instruct the data layer to fetch or persist data (whereever the data comes or goes to doesn't matter). Consider following usercase: The presentation layer presents a view where the user can create new entries (for example 10000 entries) with one action (like button click). There would be a use case in the domain layer, which will get the request from the presentation layer for creation and maybe validation. The use case will forward the creation and persistence to the data layer. In this scenario i would expect from the users perspective of view that either all entries will be created or no one, for example if the application process is terminated during write operations to the database. The data layer could create each entry one by one in own transactions or it could create all entries in one transaction and following data consistency. From my point of view only the domain layer (or the presentation layer) should determine whether all these new entries should be created in an atomic way. You should of course abstract the technical implementation detail for transactions away from the domain layer, but it is for me not clear how to abstract this logical detail.
Am i thinking wrong?
"You should of course abstract the technical implementation detail for transactions away from the domain layer"
That is correct, but what you said just before it contradicts it...
"From my point of view only the domain layer (or the presentation layer) should determine whether all these new entries should be created in an atomic way"
Whether they are created in an atomic manner or not is an implementation detail of the data layer.
The domain layer could add a method to the Repository interface such as
public abstract Observable<TrunkOfThings> updateTrunkOfThings(Thing newThing);
Now the data layer can handle the implementation details of what exactly "updating" entails, and it can hand back an updated version of the TrunkOfThings
to the domain layer, which can then convert it to a UseCase, and hand it off to the presentation layer.
I dont think that there is a contradiction, one thing is the implementation detail in the data layer like using a SqtLiteTransaction or whatever, the other thing is to pack logical operations that belongs to the domain in an abstraction (for example a set of Runnables) and pass them to the data layer and force them to run in one transaction. otherwise i dont know how you would do operations like update and delete entries from different database tables in one commit without moving business logic into the data layer.
Sorry for the late reply guys but I'm with @caseykulm a priori. Although @misrakli can you give us an specific example of what you are trying to do?
Imagin following use case. You have a flow of screen with tasks where the user can modify (insert, update, delete) data from different domain entities. At the last screen you have to save the modifications and do one commit because all captured data should be saved in an atomic way (in this case we have a local database on the device, no cloud, no webservice, etc.). The domain layer of course should not know how the data will be handled atomic, the data layer could use a database or shared preferences or whatever, but the domain layer i think must have the chance to say to the data layer "please do all this operations on the entities together or fail and rollback if there is a error". Is the concern now more clear?
@misrakli I do not see any problem here. The repository in the data layer executes the operation atomically and just in case it fails, you could throw an exception and catch it on the domain layer and do something else or just propagates it until you treat the error at a view level.
Always encapsulate the exception in an ErrorBundle: https://github.com/android10/Android-CleanArchitecture/blob/master/domain%2Fsrc%2Fmain%2Fjava%2Fcom%2Ffernandocejas%2Fandroid10%2Fsample%2Fdomain%2Fexception%2FErrorBundle.java
@android10 well the question now is how could the domain layer do this transactional operation. in my approach there is a call(Callable c) in the interface to the data layer. The domain can do all the operations (insert, update, delete, ...) in that callable and the data layer would do that within one transaction. I dont know if there is a better way to do that.
@misrakli I know I'm several years late to the party, but did you ever find a clean solution to your scenario? Because I agree with you. Making repository actions "atomic" works fine, but seems to only work if you only ever need to call one Repository method per Use Case, which seems very rare.
Consider the simple use case of, say, "Buying a Product". You'd want to perhaps deduct any store credit that the User has to use as payment (e.g: a call to a method in the "User Repository") and then create a record of the purchase (e.g: a call to a method in the "Orders Repository"). If either one fails for whatever reason, we must be able to rollback both of them, or we would be left with one very unhappy customer (e.g: their credit is deducted, but their order fails).
Even in applications with lower stakes than the example I just gave, not being able to rollback or commit all actions in one go would leave the data in an inconsistent and potentially buggy state. It is also incredibly inefficient - if you had to do multiple database operations, opening and closing multiple transactions at a time is costly.
This seems like a very common scenario that would need to be solved, so I'm very puzzled that there are so little concrete examples on how to solve it available online.
@Aetheus Nice to hear that there is still interest on this party :-) The only 'clean solution' we found for us is really to provide a run(Runnable r) in the data layer and let the use case decide which operations to pack into that runnable. You can also do it with a "unit of work", but at last you need a way to put atomic operations from the point of the use case together. The transaction costs in our case I think are not as expensive as inconsistent data are (by the way we have an offline first approach) In one of our use cases we have to decide in an atomic manner whether we can perform a specific operation on an entity or not, i.e. from the point of the use case this means
- query the entity
- check state
- modify the entiy
All this three steps have to be performed within one db transaction, otherwise inconsistent data could be the result. Maybe there are better solutions for such use cases but we ended up with the described way as we also didn't find any other approach within a reasonable time span.
Personally, I have a dao for every table in data layer. I use them in order to save/delete/retrieve data from a single table. Then i have my repositories which combine daos in order to pass data to and from domain layer (domain and data layer communicate through interfaces). As a result i handle transactions inside repositories. The code looks like that:
Completable.wrap( //call the daos )). doOnSubscribe(disposable -> db.beginTransaction()). doOnComplete(() -> db.commitTransaction()). doOnError(throwable -> db.endTransaction()). doOnTerminate(() -> db.endTransaction());
@Aetheus have you managed to derive at least a clean solution to the problem? A google search shows you posted on few sites about the scenario.
Hi @mwei0210 - in my search for an answer, I've come to 3 broad options:
-
Create another layer (a "Unit of Work") that will be responsible for instantiating the Repositories. During instantiation, it will inject the transaction context into the repositories. You can then "commit" and "rollback" this "unit of work" as you would a normal transaction. tl;dr: reinvent transactions in a wrapping layer
-
Ditch the domain layer completely. Just use an off-the-shelf ORM. You'll feel dirty, but at least things like transactions and mocking for tests are probably already accounted for.
-
Ditch transactions completely. If necessary, use a retry mechanism like queues. Let every repository action be standalone. So using my example of an "Order" and "Credit" repository, the rough flow would be: a) send a request to POST /purchase endpoint b) that endpoint then puts a "deduct credit" message into the "credit queue" and a "make order" message into the "order queue" c) the "deduct credit" message in the "credit queue" will be retried until it succeeds. Likewise for the "make order" message in the "order queue". So it wouldn't matter if Repositories can't rollback - you've essentially said "there are no rollbacks - we'll retry until we succeed". Of course, now you'd be in the realm of eventual consistency - whether or not this is acceptable for your use case is up to you.