1

I'm reading Vaughn Vernon's book on Implementing Domain Driven design. I have also been going through the book code, C# version, from his github here.

The Java version of the book has decorators @Transactional which I believe are from the spring framework.

public class ProductBacklogItemService
{
    @Transactional
    public void assignTeamMemberToTask(
        string aTenantId,
        string aBacklogItemId,
        string aTaskId,
        string aTeamMemberId)
        {
            BacklogItem backlogItem =
                backlogItemRepository.backlogItemOfId(
                    new TenantId(aTenantId),
                    new BacklogItemId(aBacklogItemId));

            Team ofTeam =
                teamRepository.teamOfId(
                    backlogItem.tennantId(),
                    backlogItem.teamId());

            backlogItem.assignTeamMemberToTask(
                new TeamMemberId(aTeamMemberId),
                ofTeam,
                new TaskId(aTaskId));
        }
}

What would be the equivalent manual implementation in C#? I'm thinking something along the lines of:

public class ProductBacklogItemService
{
    private static object lockForAssignTeamMemberToTask = new object();
    private static object lockForOtherAppService = new object();

    public voice AssignTeamMemberToTask(string aTenantId,
        string aBacklogItemId,
        string aTaskId,
        string aTeamMemberId)
        {
            lock(lockForAssignTeamMemberToTask)
            {
                // application code as before
            }
        }

        public voice OtherAppsService(string aTenantId)
        {
            lock(lockForOtherAppService)
            {
                // some other code
            }
        }
}

This leaves me with the following questions:

  1. Do we lock by application service, or by repository? i.e. Should we not be doing backlogItemRepository.lock()?
  2. When we are reading multiple repositories as part of our application service, how do we protect dependencies between repositories during transactions (where aggregate roots reference other aggregate roots by identity) - do we need to have interconnected locks between repositories?
  3. Are there any DDD infrastructure frameworks that handle any of this locking?

Edit

Two useful answers came in to use transactions, as I haven't selected my persistence layer I am using in-memory repositories, these are pretty raw and I wrote them (they don't have transaction support as I don't know how to add!).

I will design the system so I do not need to commit to atomic changes to more than one aggregate root at the same time, I will however need to read consistently across a number of repositories (i.e. if a BacklogItemId is referenced from multiple other aggregates, then we need to protect against race conditions should BacklogItemId be deleted).

So, can I get away with just using locks, or do I need to look at adding TransactionScope support on my in-memory repository?

Marcos Dimitrio
  • 6,651
  • 5
  • 38
  • 62
morleyc
  • 2,169
  • 10
  • 48
  • 108

4 Answers4

5

TL;DR version

You need to wrap your code in a System.Transactions.TransactionScope. Be careful about multi-threading btw.

Full version

So the point of aggregates is that the define a consistency boundary. That means any changes should result in the state of the aggregate still honouring it's invariants. That's not necessarily the same as a transaction. Real transactions are a cross-cutting implementation detail, so should probably be implemented as such.

A warning about locking

Don't do locking. Try and forget any notion you have of implementing pessimistic locking. To build scalable systems you have no real choice. The very fact that data takes time to be requested and get from disk to your screen means you have eventual consistency, so you should build for that. You can't really protect against race conditions as such, you just need to account for the fact they could happen and be able to warn the "losing" user that their command failed. Often you can start finding these issues later on (seconds, minutes, hours, days, whatever your domain experts tell you the SLA is) and tell users so they can do something about it.

For example, imagine if two payroll clerks paid an employee's expenses at the same time with the bank. They would find out later on when the books were being balanced and take some compensating action to rectify the situation. You wouldn't want to scale down your payroll department to a single person working at a time in order to avoid these (rare) issues.

My implementation

Personally I use the Command Processor style, so all my Application Services are implemented as ICommandHandler<TCommand>. The CommandProcessor itself is the thing looking up the correct handler and asking it to handle the command. This means that the CommandProcessor.Process(command) method can have it's entire contents processed in a System.Transactions.TransactionScope.

Example:

public class CommandProcessor : ICommandProcessor
{
    public void Process(Command command)
    {
        using (var transaction = new TransactionScope())
        {
            var handler = LookupHandler(command);
            handler.Handle(command);

            transaction.Complete();
        }
    }
}

You've not gone for this approach so to make your transactions a cross-cutting concern you're going to need to move them a level higher in the stack. This is highly-dependent on the tech you're using (ASP.NET, WCF, etc) so if you add a bit more detail there might be an obvious place to put this stuff.

Neil Barnwell
  • 41,080
  • 29
  • 148
  • 220
  • Thanks @Neil really useful. Currently I am just using repositories, which are just going to in-memory storage. I am not using any underlying storage technology that support transactions, the reason being i will write the persistence last – morleyc Nov 17 '13 at 06:49
  • 1
    TransactionScope is still the answer. If you're not using ACID persistence then you're not going to get ACID properties, but the TransactionScope should be in place (in the right layer) so that when you do have ACID storage it'll do what you need. – Neil Barnwell Nov 17 '13 at 09:37
  • So to summarize using my original example code where `backlogItemRepository.backlogItemOfId(...)` and `teamRepository.teamOfId(...)` both take an ID `aTenantId`, the transaction scope would take care of a parallel operation where a tenant with that ID is deleted whilst the transaction is underway, one of the service layer operations would fail with an exception? – morleyc Nov 17 '13 at 10:25
4

Locking wouldn't allow any concurrency on those code paths.

I think you're looking for a transaction scope instead.

Community
  • 1
  • 1
JefClaes
  • 3,275
  • 21
  • 23
1

I don't know what persistency layer you are going to use but the standard ones like ADO.NET, Entity Framework etc. support the TransactionScope semantics:

using(var tr = new TransactionScope())
{
    doStuff();
    tr.Complete();
}

The transaction is committed if tr.Complete() is called. In any other case it is rolled back.

Typically, the aggregate is a unit of transactional consistency. If you need the transaction to spread across multiple aggregates, then you should probably reconsider your model.

Bartłomiej Szypelow
  • 2,121
  • 15
  • 17
  • Thanks for the reply @Bartłomiej, here is where it gets interesting... I haven't selected the persistence layer yet - I am using an in memory repository. I would be very interested if you could point me in the direction of making my own in-memory repository transactional – morleyc Nov 15 '13 at 19:10
  • 1
    http://www.developer.com/net/net/article.php/3565196/SystemTransactions-Implement-Your-Own-Resource-Manager.htm – Bartłomiej Szypelow Nov 19 '13 at 11:18
-1
lock(lockForAssignTeamMemberToTask)
{
    // application code as before
}

This takes care of synchronization. However, you also need to revert the changes in case of any exception. So, the pattern will be something like:

lock(lockForAssignTeamMemberToTask)
{
    try {
        // application code as before
    } catch (Exception e) {
        // rollback/restore previous values
    }
}
Param
  • 2,420
  • 1
  • 14
  • 10