Skip to main content

Split Data logic and domain logic in POCO class? [Resolved]

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).

Question Credit: Steve
Question Reference
Asked August 24, 2019
Posted Under: Programming
32 views
1 Answers

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.


credit: Kain0_0
Answered August 24, 2019
Your Answer