1

Say for example, in this mock bank ATM app solution, I have 3 projects:

  • UI Console
  • Domain Model (POCO)
  • Persistence (Entity Framework for CRUD)

Domain Model (POCO): I have 2 classes

public class BankAccount
{
    public int Id { get; set; }
    public string AccountName { get; set; }
    public decimal Balance { get; set; }

    public decimal CheckBalance()
    {
        return Balance;
    }

    public void Deposit(int amount)
    {
        // Domain logic
        Balance += amount;

        // Add transaction
        var transaction = new Transaction()
        {
            Id = 1,
            TransactionDateTime = DateTime.Now,
            Amount = amount,
            TransactionType = TransactionType.Deposit
        };

        // Save changes into DB via EF. Data Logic should be here? Then where?
    }

    public void Withdraw(int amount)
    {
        // Domain logic
       if(amount < balance)
        Balance -= amount;

        var transaction = new Transaction()
        {
            Id = 2,
            TransactionDateTime = DateTime.Now,
            Amount = amount,
            TransactionType = TransactionType.Withdraw
        };

        // Save changes into DB via EF. Data Logic should be here? Then where?

    }
}

public class Transaction
{
    public int Id { get; set; }
    public DateTime TransactionDateTime { get; set; }
    public TransactionType TransactionType { get; set; }
    public int Amount { get; set; }
}

public enum TransactionType
{
    Deposit, Withdraw
}

Console UI

class Program
{
    static void Main(string[] args)
    {
        var bankAccount = new BankAccount() { Id = 1, AccountName = "John", Balance = 250M };

        Console.WriteLine("1. Check balance");
        Console.WriteLine("2. Deposit");
        Console.WriteLine("3. Withdraw");
        Console.WriteLine("Enter option: ");
        string opt = Console.ReadLine();
        switch (opt)
        {
            case "1":
                Console.WriteLine($"Your balance is ${bankAccount.CheckBalance()}");
                break;
            case "2":
                bankAccount.Deposit(50);
                Console.WriteLine("Deposit successfully");
                break;
            case "3":
                bankAccount.Withdraw(20);
                Console.WriteLine("Withdraw successfully");
                break;
            default:
                break;
        }

    }
}

I have few questions here.

  1. If a pure method suppose to just do one thing, does my Deposit method violate this principle? If yes, how it should be then?
  2. In POCO class, I have the domain logic as comment. But should not have my data logic (CRUD operation) there right? Then where should I put the data logic. For example, db.SaveChanges (for Entity Framework).
1

The three examples you gave all have the same operational model: Request, Update, Respond.

Essentially they are CRUD operations and so we could answer your questions as:

  1. It is doing one thing: the U in CRUD.
  2. Why not? The purpose of this business logic is to perform a simple update of a single domain entity. There is no reason to distribute the logic on such a concise usage.

These answers are accurate and good... for the current context. They are as shallow as the examples.

It might not look like it but much of your interesting business logic is going to occur between domain entities.

So lets imagine another scenario in your main class.

class Program
{
    static void Main(string[] args)
    {
        var bankAccount = new BankAccount() { Id = 1, AccountName = "John", Balance = 250M };
        var bankAccount2 = new BankAccount() { Id = 2, AccountName = "Bill", Balance = 50M };

        Console.WriteLine("1. Check balance");
        Console.WriteLine("2. Deposit");
        Console.WriteLine("3. Withdraw");
        Console.WriteLine("4. Transfer");
        Console.WriteLine("Enter option: ");
        string opt = Console.ReadLine();
        switch (opt)
        {
            case "1":
                Console.WriteLine($"Your balance is ${bankAccount.CheckBalance()}");
                break;
            case "2":
                bankAccount.Deposit(50);
                Console.WriteLine("Deposit successfully");
                break;
            case "3":
                bankAccount.Withdraw(20);
                Console.WriteLine("Withdraw successfully");
                break;
            case "4":
                bankAccount.Withdraw(20);
                bankAccount2.Deposit(20);
                Console.WriteLine("Transferred successfully");
                break;
            default:
                break;
        }

    }
}

Take a careful look at option 4.

I can foresee issues here. Namely what happens when a power outage happens, or an out of memory error occurs post transactional commit for Withdraw, but pre-commit for Deposit?

Notice that the usage has changed, transfer is not a CRUD operation. It is a more complicated operation and needs to handle persistence differently.

So now the answers have changed:

  1. It is conflating three separate levels:

    • Communication: Request, Begin Transaction, Operation, Commit Transaction, Response
    • Business Process: The set of Business Operation like Transfer Funds that have to be handled by orchestrating domain operations and domain entities such as withdraw/deposit on various accounts.
    • Domain Entities: The primitive interactions and invariants. Such as an account cannot be overdrawn, or a fixed-term account cannot be withdrawn until it matures.
  2. Given the new separation of concerns it does not make sense to keep the responsibilities where they are.

    • It can still (within this context) makes sense for each domain object to interact with the transaction to update the database.
    • But transaction control needs to be lifted to the level of Request handling.

These answers work for this context, but will breakdown when faced with different problems:

  • Multi-stage/long running Workflows
  • Batch processing

As an aside

When I'm talking about a domain object persisting itself, my imagination summons up:

interface Repository<T>
{
    guid create(T item);
    void update(T item);
    bool delete(guid item_id);
    IList<T> find(map<string, object> key_to_value);
    T? load(guid item_id);
}

And that the Domain object somehow can locate/be given an implementation of the IRepository with which to operate. (Perhaps several different repositories for the Ts it need to work with).

The worst thing you can do is tie your unique hand crafted business logic (the domain) to a framework that will eventual be decommissioned/abandoned/replaced.

  • For my data validation, say input amount has to be greater than zero, it will be at repository right? – Steve Aug 16 at 13:12
  • That is probably a bad idea. The repository can change, that is the whole point of placing it behind an interface (take a look at onion/hexagonal architecture). What that means is that every repository would have to know that rule. Where as if that validation was in the domain entities then it only needs to be known in one place. If you were to change the domain entities, you probably also want to change the rule so that makes more sense. – Kain0_0 Aug 19 at 1:00

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.