1

Originally posted at CodeReview, but it was brought up that this would be better posted here. The other question has been deleted.

Creating some personal finance software, but running into an architectural issue. I have a class called Account that holds Transactions as well as SubAccounts. I'm very early into the coding phase, but I'm seeing that my Account class could potentially become very large.

This is what I have:

public class Account
{
    public Account(string name)
    {
        Name = name;
        _subAccounts = new List<Account>();
        Transactions = new List<Transaction>();
    }

    public string Name { get; }
    public List<Transaction> Transactions { get; }

    public IEnumerable<Account> SubAccounts => _subAccounts;

    public decimal Balance => ComputeBalance();

    public void AddSubAccount(Account account)
    {
        Debug.Assert(account != null);

        _subAccounts.Add(account);
    }

    public Account FindSubAccount(string name)
    {
        // logic to find account
        return account;
    }

    public void RemoveSubAccount(Account account)
    {
        Debug.Assert(account != null);
        _subAccounts.Remove(account);
    }

    public void AddTransaction(Transaction transaction)
    {
        Transactions.Add(transaction);
    }

    public void FindTransactionByID(TransactionID id)
    {
        // logic to find transaction
        return transaction;
    }

    public void RemoveTransaction(Transaction transaction)
    {
        Transactions.Remove(transaction);
    }

    private decimal ComputeBalance()
    {
        decimal balance = 0M;

        foreach (var transaction in Transactions)
        {
            balance += transaction.Amount;
        }

        return balance;
    }

    private readonly List<Account> _subAccounts;
}

public class Transaction
{
    public Transaction(string desc, decimal amount, Account debit, Account credit)
    {
        Description = desc;
        Amount = amount;
        Credit = credit;
        Debit = debit;
    }

    public string Description { get; }
    public decimal Amount { get; }
    public Account Credit { get; }
    public Account Debit { get;  }
}

The issue that is starting to appear is that my Account class seems very segregated between 3 parts: SubAccounts, Transactions, and computing a Balance. However, in terms of talking about accounting, a financial account has all these attributes.

My question is: what to do to prevent a class from becoming too large (too many responsibilities) when trying to stick to the ubiquitous domain language?

To fix this, I had the idea of creating a tree data structure that would use AccountNodes that hold pointers to other AccountNodes (to simulate SubAccounts) as well as Account. This would separate the physical hierarchical structure from the account.

1

Your Account class concept is fine. Obviously you have a technical limitation, specifically with the Transactions list, which could obviously become too large to performantly populate and hold in memory.

However, you can solve this technical limitation without drastically changing your class design by injecting a Domain Service for Transactions and altering your method signatures slightly to take search parameters. ie

public class Account
{
    public Account(ITransactionService trans)

    public IEnumerable<Transaction> GetTransactionsByDate(DateTime start, DateTime end)
    {
        return trans.GetTransactionsByDate(this.Id, start,end);
    }
}

By doing this you have effectively created a new bounded context for Transaction. we are allowing Transactions to exist outside of Accounts and joining the two contexts with a Domain Service

  • I plan on being able to display all Transactions for that account into some type of GridView. I believe this would require all Transactions to be read into memory. – keelerjr12 Sep 29 '18 at 19:12
  • @keelerjr12, that's a common misconception when starting to work with DDD. Using your Domain Models for reads is a gateway to performance hell. The reading should be "outsourced" to a completely different layers, which e.g. uses performant plain SQL queries and maps results to simple DTOs to be used in views (and/or data-grids as in your case). Simple DTOs will have no problem fitting into the memory at all. On better domain modelling, check out these excellent resources by Vaugh Vernon. – Andy Sep 29 '18 at 19:26
  • @Andy, isn't full-blown DDD overkill for a small desktop application? – keelerjr12 Sep 29 '18 at 19:30
  • @keelerjr12, that's another thing. It is. :) The design usually only starts to make sense when you start implementing more complex domain rules, very often accompanied by event dispatching and its handling. Until then, completely ditching the idea of having your business logic in entities in favour of having them in service classes makes your design much simpler and easier to use. – Andy Sep 29 '18 at 19:33
  • @keelerjr12 you can always add a 'read all' if required, but generally you only want to show a page at a time. – Ewan Sep 29 '18 at 19:43
1

Your Account class is merely a dressed-up Repository. All it contains is CRUD methods, for the most part. The only method in that class that does any real work is the ComputeBalance method.

To correct the problem, remove the CRUD methods from Account (relegating them to a Repository), and inject a DTO into the constructor that contains the necessary data for computing the balance.

This separates the concern about where and how to get the data into your Data Access layer, where it belongs.

  • My plan was to have a JournalService that uses a JournalRepository to perform CRUD to get a Journal. The Journal holds a List of Accounts which holds a List of SubAccounts and a List of Transactions. – keelerjr12 Sep 29 '18 at 19:25
  • Sounds like the Journal object would be the DTO I described. – Robert Harvey Sep 29 '18 at 19:34
  • Journal DTO or Transaction DTO? What about injecting a TransactionService? Injecting a TransactionService into an aggregate seems wrong though. – keelerjr12 Sep 30 '18 at 1:46
  • It's common for new practitioners of DDD to over-complicate things. – Robert Harvey Sep 30 '18 at 17:59
1

Domain of personal finance

In the the simplified domain of personal finance an Account usually represents an income and expenditure account, such as for example a cash account (your wallet) or a bank account.

In this domain, the Account could be an aggregate root:

  • It can be divided into sub-accounts (current account, payment card account, deposit account, etc). Some banks write for example on the same account statement, the operations of a payment card and operations of the current accounts. Other banks use a sub-account suffix and the suffix makes only sense with the main account.

  • Transactions are individual change of values in the (sub-)accounts. Every single line is uniquely identified only in relation with the account number.

In this context, your model could be valid. The AddSubAccount() and FindTransactionPerID() would not repository operations, but access to aggregate entities via the aggregate root.

However there are several inconsistencies with Transaction that suggest to reconsider seriously your domain model.

Core design question: Relation between transactions and accounts

Looking more closely at your Transaction, it appears that each transaction is related to two accounts, with only one amount. This suggests that transactions should not belong to an account aggregate, but should be an independent aggregate stored in their own TransactionRepository.

Moreover, it appears that sub-accounts are themselves accounts and can directly appear in transactions. This suggest that each sub-account is an independent account that can be accessed directly. So they should be added and found back from the AccountRepository

Another suggestion would be to consider a more powerful transaction model, that would allow to have multiple amounts and accounts in a single transaction. This could for example to have one expenditure on the bank account, but to allocate it to several expense accounts (e.g. food, drinks, clothes). But maybe this last one is for the next release ;-)

  • This is exactly how I did it. Thank you for this awesome answer, but due to Ewan's speedy response, I have chosen to accept his as it helped me understand DDD. – keelerjr12 Sep 30 '18 at 18:44
  • No problem! I'm writing answers to help and not win ;-) I think that every answer here went through a different but complementary view point. – Christophe Sep 30 '18 at 19:14
1

I like your idea of moving a part of the logic to a separate data structure. To not reinvent the wheel, I would use the semantics provided by ICollection<T>. This has also the advantage that you can apply known techniques to it like looping with foreach or querying with LINQ.

Both, account and transaction collections have a common logic like being able to be searched by Id. Therefore, I would create a special interface for these things, which in turn implements ICollection<T>:

public interface IDomainCollection<T> : ICollection<T>
{
    T FindById(int id);
    //TODO: Add other common logic
}

A substantial part of this logic can be implemented in an abstract base class:

public abstract class DomainCollection<T> : IDomainCollection<T>
{
    protected readonly List<T> _innerList = new List<T>();

    public abstract T FindById(int id);

    // ICollection<T> members
    public int Count => _innerList.Count;
    public bool IsReadOnly => false;
    public void Add(T item) => _innerList.Add(item);
    public void Clear() => _innerList.Clear();
    public bool Contains(T item) => _innerList.Contains(item);
    public void CopyTo(T[] array, int arrayIndex) => _innerList.CopyTo(array, arrayIndex);
    public bool Remove(T item) => _innerList.Remove(item);
    public IEnumerator<T> GetEnumerator() => _innerList.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_innerList).GetEnumerator();
}

Now the two required collections can be implemented easily:

public class AccountCollection : DomainCollection<Account>
{
    public Account FindByName(string name) // Specific to accounts
    {
        throw new NotImplementedException();
    }

    public override Account FindById(int id)
    {
        throw new NotImplementedException();
    }
}

public class TransactionCollection : DomainCollection<Transaction>
{
    public override Transaction FindById(int id)
    {
        throw new NotImplementedException();
    }
}

The Account class's logic is reduced to balancing logic:

public class Account
{
    public Account(string name)
    {
        Name = name;
    }

    public string Name { get; }

    public TransactionCollection Transactions { get; } = new TransactionCollection();

    public AccountCollection SubAccounts { get; } = new AccountCollection();

    public decimal Balance => ComputeBalance();

    private decimal ComputeBalance()
    {
        decimal balance = 0M;
        foreach (var transaction in Transactions) {
            balance += transaction.Amount;
        }
        return balance;
    }
}

The semantics for sub-accounts and transactions changes from

account.AddSubAccount(newAccount);
Account subAccount = account.FindSubAccount(name);
Transaction transaction = account.FindTransactionByID(id);

to

account.SubAccounts.Add(newAccount);
Account subAccount = account.SubAccounts.FindByName(name);
Transaction transaction = account.Transactions.FindById(id);

You are still in full control of what these collections do. You can add debug assertions and perform additional logic if required.

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.