aetheus
aetheus

Reputation: 761

How do you use transactions in the Clean Architecture?

No implementations of it that I can find online actually give you a framework agnostic and practical way of implementing it.

I've seen several subpar suggestions towards solving it:

  1. make Repository methods atomic

  2. make Use Cases atomic

Neither of them are ideal.

Case #1: most Use Cases depend on more than a single Repository method to get their job done. When you're "placing an order", you may have to call the "Insert" methods of the "Order Repository" and the "Update" method of the "User Repository" (e.g: to deduct store credit). If "Insert" and "Update" were atomic, this would be disastrous - you could place an Order, but fail to actually make the User pay for it. Or make the User pay for it, but fail the Order. Neither are ideal.


Case #2: is no better. It works if each Use Case lives in a silo, but unless you want to duplicate code, you'll often find yourself having Use Cases that depend on the operation of other Use Cases.

Imagine you have a "Place Order" use case and a "Give Reward Points" use case. Both use cases can be used independently. For instance, the boss might want to "Give Reward Points" to every user in the system when they login during your system's anniversary of its launch day. And you'd of course use the "Place Order" use case whenever the user makes a purchase.

Now the 10th anniversary of your system's launch rolls by. Your boss decides - "Alright Jimbo - for the month of July 2018, whenever someone Places an Order, I want to Give Reward Points".

To avoid having to directly mutate the "Place Order" use case for this one-off idea that will probably be abandoned by next year, you decide that you'll create another use case ("Place Order During Promotion") that just calls "Place Order" and "Give Reward Points". Wonderful.

Only ... you can't. I mean, you can. But you're back to square one. You can guarantee if "Place Order" succeeded since it was atomic. And you can guarantee if "Give Reward Points" succeeded for the same reason. But if either one fails, you cannot role back the other. They don't share the same transaction context (since they internally "begin" and "commit"/"rollback" transactions).


There are a few possible solutions to the scenarios above, but none of them are very "clean" (Unit of Work comes to mind - sharing a Unit of Work between Use Cases would solve this, but UoW is an ugly pattern, and there's still the question of knowing which Use Case is responsible for opening/committing/rolling back transactions).

Upvotes: 58

Views: 15273

Answers (3)

khaled  Dehia
khaled Dehia

Reputation: 947

I know this is old question, but hopefully this answer will help someone looking for sample implementation Since Clean Arch all layers pointing inward not outward

All Layers pointing inward

So since the transaction is a concern of the application layer I would keep it in the application layer.

In the quick example I made the application layer has the interface IDBContext that has all my Dbsets I will use as following

public interface IDBContext
{
    DbSet<Blog> Blogs { get; set; }
    DbSet<Post> Posts{ get; set; }
    Task<int> SaveChangesAsync(CancellationToken cancellationToken);
    DatabaseFacade datbase { get; }
}

and in persistence layer I have the implementation of that interface

public class ApplicationDbContext : DbContext, IDBContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) :base(options)
    {

    }
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
    public DatabaseFacade datbase => Database;


    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
    {
        
        var result = await base.SaveChangesAsync(cancellationToken);
        return result;
    }
}

Back to my application layer which I normally use IMediator and follow CQRM so I made this example which I hope it might be helpful here is the line where I start the transaction

await using var transaction = await context.datbase.BeginTransactionAsync();

here is the command handler where I am using transaction

public async Task<int> Handle(TransactionCommand request, CancellationToken cancellationToken)
    {
        int updated = 0;
        await using var transaction = await context.datbase.BeginTransactionAsync();
        try
        {
            var blog = new Core.Entities.Blog { Url = $"Just test the number sent = {request.number}" };
            await context.Blogs.AddAsync(blog);
            await context.SaveChangesAsync(cancellationToken);

            for (int i = 0; i < 10; i++)
            {
                var post = new Core.Entities.Post
                {
                    BlogId = blog.BlogId,
                    Title = $" Title {i} for {blog.Url}"
                };
                await context.Posts.AddAsync(post); 
                await context.SaveChangesAsync(cancellationToken);
                updated++;
            }

            var divresult = 5 / request.number;
            await transaction.CommitAsync();
            
        }
        catch (Exception ex)
        {
            var msg = ex.Message;
            return 0;
            
        }
        return updated;


    }

here is the Link for the sample I just created to explain my answer in details

please Keep in mind I did this example in like 15 min just as something to refer to in case there are some bad naming :)

Regards,

Upvotes: 2

Alvaro Arranz
Alvaro Arranz

Reputation: 454

Generally, it is recommended to place the transactions definition in the UseCase layer because it has the propper level of abstraction and has the concurrency requirements. In my opinion the best solution is the one you expose in Case#2. To solve the issue of reusing different UseCases, some frameworks use the concept propagation in Transactions. For instance, in Spring, you can define the Transactions to be REQUIRED or REQUIRES_NEW.

https://www.byteslounge.com/tutorials/spring-transaction-propagation-tutorial

If you define Transactional REQUIRED in the UseCase PlaceOrderAndGiveAwards, the transaction is reused in the 'base' usecases and a rollback in the inner methods will make the hole transaction to rollback

Upvotes: 3

Virmundi
Virmundi

Reputation: 2631

I put the transaction on the controllers. The controller knows about the larger framework since it probably has at least metadata like annotations of the framework.

As to the unit of work, it’s a good idea. You can have each use case start a transaction. Internally the unit of work either starts the actual transaction or increases a counter of invoked starts. Each use case would then call commit or reject. When the commit count equals 0, invoke the actual commit. Reject skips all of that, rolls back, then errors out (exception or return code).

In your example the wrapping use case calls start (c=1), the place order calls start(c=2), place order commits (c=1), bonus calls start (c=2), bonus calls commit (c=1), wrapping commits (c=0) so actually commit.

I leave subtransactions to you.

Upvotes: 10

Related Questions