3

I have an entity that has a dollar balance property. Whenever an event occurs that causes the balance to change, such as a debit, within the same transaction the balance should be decremented by the amount of the debit (as a sort of "summary" value, so that a DB query won't need to be performed to retrieve the balance every time.)

There are two ways to do this:

  1. I can use lock for update functionality in the DB to prevent any reads from the balance value until it finishes processed. That way, I can change the balance in my entity, such as $user->lowerBalance(1000), and then commit the value in a repository. The downside is this can be a high-load application, and I don't want to stop anyone from reading the value.
  2. The other option is to use the database's decrement feature: update users set balance = balance - 1000. This will ensure that even if two updates are made at nearly the same time, the value will always be correct. The actual value will not be set or known in the domain layer until it queried.

I want to use option 2, but how do I model this? One way is to call the repository's method from within the lowerBalance() method in the Entity's class, but then I've coupled the domain entity to the infrastructure layer. Plus, if I want the latest value, I'd need to ensure the repository re-fetches the entity. Maybe it's ok in this case, as the blue book says that layers can depend on the layers below them.

Any suggestions on how this can be structured?

  • What happens when the user has 1000 balance and you execute the second option twice at the same time? – Kayaman Jun 7 '18 at 6:05
  • "Maybe it's ok in this case, as the blue book says that layers can depend on the layers below them" - The Domain layer should not depend on any of the other layers, no matter what the blue books says – Constantin Galbenu Jun 7 '18 at 6:42
  • 1
    If you are modelling a bank account, model how banks track account activity. An account is a sequence of credits and debits. The balance is calculated, from the sum of that sequence – Caleth Jun 7 '18 at 8:20
  • @Kayaman database transactions ensure that only one will be processed at a time, so the final value will be as expected. – timetofly Jun 8 '18 at 12:57
  • @ConstantinGalbenu what does that mean in practice? An interface to the infrastructure layer has to be injected somewhere in the model. Is that not a dependency? – timetofly Jun 8 '18 at 12:58
1

Your whole application is supposed to consist of things that are business relevant, not just a limited set of "domain objects". This insight might actually conflict with the blue book, which endorses a layered architecture and the existence technical objects.

What I'm saying is, that some business-functions might naturally be implemented as an external function of the database. This comes naturally for designs that do not model the data, but the function.

A design I did in Java a while back:

public interface Customer {
    void freezeCreditCards();
}

This is an interface that is business-relevant. The freezeCreditCards() is straight from the requirements document. Now, instead of modeling the data further, like creditcards, card numbers, this was the implementation:

public final class DatabaseCustomer {
    private final long customerId;

    public DatabaseCustomer(long customerId) {
        this.customerId = customerId;
    }

    @Override
    public void freezeCreditCards() {
        sql("update creditcard set frozen = 1 where customerid = :customerid", customerid);
    }
}

The DatabaseCustomer had no "data" about the cards, it didn't need to. Sure, it now knows about the database. But, will this application ever change the database to something different? Maybe, but certainly not often.

Here is a presentation of mine about this line of thought, called Object-Oriented Domain-Driven Design.

Summary: You can keep the balance in the database, nothing wrong with that. Create an appropriate abstraction (object) with business-relevant methods to manipulate it, and you're fine.

  • Thanks for the advice. I went through your presentation, there's a lot of food for thought. – timetofly Jun 8 '18 at 13:20
0

In any modern DBMS, there is practically no need to use locking constructs directly.

UYou say

Whenever an event occurs that causes the balance to change ... within the same transaction

Do you mean a Business transaction or, more usefully, a Database one?

So, if you were transferring funds (or whatever) between two different users, you'd do something like this:

begin transaction ; 

update users set balance = balance - :value where user_id = :user1 ; 
update users set balance = balance + :value where user_id = :user2 ; 

-- other updates that might be required 

commit ; 

These are guaranteed by the DBMS to happen as a single unit of work, all or not at all.

  • Furthermore, with regards to the last sentence in Q1, most RDBMs* will not prevent a read operation on the values being updated. It will give you the value(s) currently committed irrespective on any in-flight transactions. I'd go as far to say that the questioner needs to gain some more substantial understanding of how databases work and how to interact with them. There seems to be some missing fundamentals. (*Old versions of SQL server will block and give wrong information and you can still make this happen if you configure it so) – LoztInSpace Jun 7 '18 at 11:57
  • I am well aware of how database transactions work. My question was specifically regarding how to architect it in the code with proper separation of layers as Eric Evans teaches in his book, e.g. how to make it obvious in the domain model without coupling to the database. – timetofly Jun 8 '18 at 13:04
0

Id reccomend that you add a row for each transaction and calculate the balance on the fly as it has a number if advantages.

The most important is the ability to reproduce the balance accurately. You can see where the balance came from. This is important from an auditing and fraud prevention perspective.

Also having time stamped transactions makes it easier when calculating monthly sales for accounting purposes.

It also allows you provide adjustments and reversals, which can be displayed on an invoice or statement.

As others have mentioned, the db should guarantee the transaction.. but accidents still happen. Its possible 2 transactions could be competing for a lock and 1 transaction becomes a deadlock victim. Depending on how well your app handles errors would it try again? Assume it was successful or die? How would you know if it had updated the balance or not before it failed?

I would be designing it in such a way that if it recovered from failure or crash you can be confident to resume.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.