1

I want to atomically update a record to maintain consistency, it could be done easily with the following SQL.

UPDATE account SET balance = balance - 25 WHERE id = '5' AND balance > 25

or NoSQL (MongoDB)

db.account.update({_id: 5, balance: {$gte: 5}}, {$inc: {balance: -5}})

But in DDD, how can I do this properly without a race condition problem? This should be fast so explicit lock should be avoided. (Unless unavoidable for the sake of DDD.)

Race condition may occur

class ApplicationService
  func creditAccount(String accountId, int amount)
    let account = accountRepository.find(accountId)
    account.credit(amount)
    accountRepository.save(account)

Double dispatch?

class ApplicationService
  func creditAccount(String accountId, int amount)
    let account = accountRepository.find(accountId)
    account.credit(amount, accountDao)

class Account
  func credit(int amount, AccountDao dao)
    this.balance = dao.adjustAmountAndReturn(this.id, -amount)

EDIT

I know that the risk of race condition is low but the damage is high, thus probability multiply by cost still results high expected value.

In this question, I'm looking for a solution that fits both RDBMS and NoSQL.

iwat
  • 3,591
  • 2
  • 20
  • 24
  • 1
    Hard to answer without knowing the persistence stack in question. This is also more a concurrency control problem than a DDD problem. – guillaume31 Dec 21 '16 at 14:07
  • Instead of trying to "solve" the race condition problem, you may want to ask yourself why it is so important to ensure the consistency here. Is it really critical? Can't you do some consolidation scripts if it happens? What is the frequency of the race conditions? If it happens, what can you do to in real life to face the problem? Asking those questions will make your own more DDD related ;) – Boris Guéry Dec 21 '16 at 17:06
  • @BorisGuéry While these advices are true for large distributed systems, where strong consistency is technically very challenging, I find that it shouldn't be the default way of thinking. Preventing overdrafts rather than dealing with them after the fact is probably much simpler and desirable in most domains. – plalx Dec 21 '16 at 18:26
  • @plalx, that is a discutable point of view, in my own experience, I realised that most of the time inconsistency is not a problem and can be handled in a different way, most developers spend their time finding a way to ensure it while it would have been simpler to just deal with it and take proper action when it happens. Btw consistency has a wide definition, are we talking about the write consistency (ie: what really happened and in which order), read consistency (ie: a stale cache). Most of the time consolidation scripts can be easy to implement, but it may require a different modelling – Boris Guéry Dec 21 '16 at 18:58
  • @plalx Preventing inconsistency in a domain is done by proper modelling and ensuring invariant and consistent object instances, but dealing with stale data is something different, as in real life, some mails can be delayed while some other are not and all you can do is "deal with it" and apply proper action (sometime manually, sometimes it can be automated, a cron script for example). – Boris Guéry Dec 21 '16 at 19:02
  • @BorisGuéry I am well aware of all this. I'm just saying that a lot of people are advocating to take **true invariants** and make them **eventually consistent** instead and in my opinion that is overcomplicating things for small-scale systems. It's certainly much easier to prevent an overdraft than having to deal with overdrafts after the fact, by charging fees for instance, unless that is really what the business wants to do. For example, I doubt that SO would allow concurrent bounties to exceed the total reputation I have. – plalx Dec 21 '16 at 19:13
  • You may say that in theory there is very little risk of having a race condition in this scenario, but that is under normal conditions (e.g. not programmatically abused). The first rule of distributed systems is **don't do it** and it should probably be the same for eventual consistency, unless the technical complexity, concurrency conflict risks or performance hit is too high. – plalx Dec 21 '16 at 19:15
  • @plalx, got it, and agree, as always, I think we need to find the good ratio between the frequency of errors and the cost of handling it (in terms of both development (complexity) and business (fees)). I just wanted to point that some of the time it is good to think of the problem in another way and focus on what it really matters. – Boris Guéry Dec 21 '16 at 20:04
  • @BorisGuéry I believe that the probability of race condition is low, but it costs high damage: overdraft (unsecured loan), reputation, and may even break regulations. For my use case, this must be handled very strictly. – iwat Dec 22 '16 at 02:19
  • @iwat This is what I'm saying. In order to know which implementation would work for you we would have to know exactly which DB you will be using, but the most common approach is to use a form of optimistic locking (you can google that). – plalx Dec 22 '16 at 02:19
  • @plalx Sorry, I did mentioned a wrong person. But even with optimistic locking, I still need complex update e.g., `UPDATE account SET balance = balance - 25, version = 9 WHERE id = '5' AND version = 8` unless it's natively supported by ORM framework. – iwat Dec 22 '16 at 02:25
  • @iwat That's not how it works. The business logic is executed by your model, the `Account` object gets mutated in-memory and checks it's invariants. Then you save changes back. The version will ensure no once touched the account since data was read. Basically, you would have something like `void debit(int amount) { if (balance < amount) throw new InsufficientFunds(); balance -= amount; }`. – plalx Dec 22 '16 at 02:31
  • @plalx I see, the domain should process the new balance value, this is very domain driven. But how it work on optimistic locking? – iwat Dec 22 '16 at 02:37
  • @iwat You either need an ORM that supports optimistic locking out of the box (e.g. Hibernate) or you must implement that yourself in your repository implementation. – plalx Dec 22 '16 at 02:38
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/131206/discussion-between-iwat-and-plalx). – iwat Dec 22 '16 at 02:42

1 Answers1

1

I think you can have a domain service that you implement with that query like:

interface CreditAccountService {
    void operation(String accountId, int amount);
}

class DbCreditAccountService implements CreditAccountService{
    void operation(String accountId, int amount){
        db.executeQuery("UPDATE account SET balance = balance - "+amount+" WHERE id = '"+accountId+"' AND balance > " + amount);
    }
}

But maybe is better manage the race condition using a locking mechanism.
If your application isn't distributed at all a pessimistic locking using the entity id will be enough to prevent race conditions...
If you application is replicated in 'n' server and has a consistent database you can use an optimistic lock in the db table.
Or maybe a distributed lock if also your db is distributed and eventual consistent.

You can have more info on pessimistic/optimistic locking here

Community
  • 1
  • 1
rascio
  • 8,968
  • 19
  • 68
  • 108
  • I discussed with more people and it seems that putting domain knowledge into persistent/infrastructure layer should be avoided, do you agree? – iwat Dec 30 '16 at 03:10
  • I partially agree :) The important thing (for me) is to not make the "domain knowledge" hidden in the layer. I think this is the services responsability, you make the operation explicit (with an interface) but its implementation maybe is optimized with a sql query. You have to evaluate the trade off. Implementing the operation using your domain object maybe is safer (having invariants implemented in object), but if you find that it's a lot harder to manage other stuffs maybe the query solution is the convenient choice. – rascio Dec 30 '16 at 15:15
  • Do you have this problem just for this operation? If you have for more, maybe implementing a locking mechanism is more convenient. If it is just this maybe right now is preferable do it with a query (faster dev) than resolve harder problems. – rascio Dec 30 '16 at 15:16