1

I am trying to apply Domain-Driven Design here in this bank ATM mock app.

I have 3 layers at the moment:

  • Domain Model Entity layer (Pure POCO classes)
  • Persistence layer (Entity Framework)
  • Console UI layer

In Persistence layer, I have RepositoryTransaction class which perform CRUD operation.

namespace ATM.Persistance.Repository
{
public class RepositoryTransaction
{
    private readonly AppDbContext db;

    public RepositoryTransaction()
    {
        db = new AppDbContext();
    }

    public int AddTransaction(Transaction transaction)
    {
        db.Transactions.Add(transaction);
        return db.SaveChanges();
    }
}
}

In my Domain Model Entity layer, I have BankAccount class.

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

    // To create RepositoryTransaction to call AddTransaction method.
    // But according to Domain-Driven Design, domain model layer should not depend on
    // any layer, right? But here, it depends on Persistance layer.
    // If it doesn't depend on Persistance layer, then where do I do so? In Console UI Core?
    // private readonly RepositoryTransaction repoTrans = new RepositoryTransaction();

    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
        };

        // repoTrans.AddTransaction(transaction);
    }

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

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

        // repoTrans.AddTransaction(transaction);
    }
}

and in Console UI layer, I have the Program class and the Main method:

namespace ATM.ConsoleUICore
{
class Program
{
    static void Main()
    {
        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":
                // User to input amount.
                // Data validation to make sure amount is greater than zero.

                bankAccount.Deposit(50);
                Console.WriteLine("Deposit successfully");
                break;
            case "3":
                // User to input amount.
                // Data validation to make sure amount is greater than zero.

                bankAccount.Withdraw(20);
                Console.WriteLine("Withdraw successfully");
                break;
            default:
                break;
        }

    }
}
}

My question is written in the BankAccount class. According to Domain-Driven Design, domain model layer should not depend on any layer, right? But here, it depends on Persistence layer. If it doesn't depend on Persistence layer, then where do I do so? In Console UI layer?

2

It is a good practice to introduce an intermediate "Application Service" layer between your controller (here, your Console UI) and the Domain Layer.

The App Service will be responsible for invoking the (injected) infrastructure services, calling the domain layer and also persisting/loading necessary data. The Controller's responsibility is only to gather the request params (gather user input in your case), ensure authentication (if needed), and then make the call to the Application Service method.

Application Services are the direct clients of the domain model and act as intermediaries to coordinate between the external world and the domain layer. They are responsible for handling infrastructure concerns like ID Generation, Transaction Management, Encryption, etc. Such responsibilities are not concerns of the Controller layer either.

App services typically do task coordination for APIs (one service method per API). In your case, an API would translate to each option you expose in the Console UI. When using an ACID database, the same Services also control transactions, ensuring that model state transitions are atomically persisted.

In your specific example, the App Service would be responsible for invoking the BankAccount operations, after getting input from the console layer. It loads data from DB using the persistence layer, when needed. After the domain layer completes its operation, app service persists the data again using the persistence layer. And returns info to the console UI.


Code (almost psuedocode) Example

If BankAccount and Transaction are both Aggregates, they should be updated in different transactions. It also means that both BankAccount and Transaction have separate repositories. I am not well-versed with C#, so I have taken cues from your earlier code, and expanded it to include Application Services, Domain Events and Subscribers. I am assuming here that you are aware of these domain elements from books/materials on DDD.

namespace ATM.Persistance.Repository
{
    public class RepositoryTransaction
    {
        private readonly AppDbContext db;

        public RepositoryTransaction()
        {
            db = new AppDbContext();
        }

        public int AddTransaction(Transaction transaction)
        {
            db.Transactions.Add(transaction);
            return db.SaveChanges();
        }
    }
}


namespace ATM.Persistance.Repository
{
    public class RepositoryBankAccount
    {
        private readonly AppDbContext db;

        public RepositoryBankAccount()
        {
            db = new AppDbContext();
        }

        public findById(int bankAccountId)
        {
            return db.BankAccounts.find(bankAccountId);
        }

        public int AddBankAccount(BankAccount account)
        {
            db.BankAccounts.Add(account);
            return db.SaveChanges();
        }

        public int UpdateBankAccount(BankAccount account)
        {
            db.BankAccounts.Update(account);
            return db.SaveChanges();
        }
    }
}

namespace ATM.Messaging
{
    // A message broker is typically used to carry domain events
    //  from one aggregate to another. But you could simply use a
    //  a pub-sub mechanism if you have just one bounded context
    public class MessageBroker
    {
        private readonly MessageQueue mq;

        public MessageBroker()
        {
            mq = new MessageContext();
        }

        public void Publish(Event newEvent)
        {
            mq.push(newEvent)
        }
    }
}

namespace ATM.Domain
{
    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;
        }

        public void Withdraw(int amount)
        {
            // Domain logic
            Balance -= amount;
        }
    }

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

namespace ATM.Messaging.Subscribers
{
    // Subscribers are classes that let the domain know
    //  they are interested in a specific domain event.
    //  They are automatically notified when the event is raised.

    public class NewTransactionSubscriber
    {
        // This method is invoked by the pub-sub mechanism
        //  when BankAccountUpdated event was raised
        public void notify(BankAccountUpdated event)
        {
            // Even subscribers go through Application Services
            TransactionService.AddNewTransaction(
                1, DateTime.now(), event.bankAccountId,
                event.amount, event.transactionType)
        }
    }
}

namespace ATM.ApplicationService
{
    public class AccountService
    {
        public void DepositAmount(int bankAccountId, int amount)
        {
            UnitOfWork uow = new UnitOfWork();
            uow.begin();

            // repoBankAccount is injected into the application service
            //  or you might be just instantiating it here
            bankAccount = repoBankAccount.findById(bankAccountId)

            bankAccount.Deposit(amount)

            repoBankAccount.UpdateBankAccount(bankAccount)

            //You fire a domain event mentioning that the BankAccount has been updated
            messageBroker.Publish(new BankAccountUpdated(bankAccountId, amount, "DEPOSIT"))

            uow.commit();
        }

        public void WithdrawAmount(int bankAccountId, int amount)
        {
            UnitOfWork uow = new UnitOfWork()
            uow.begin()

            // repoBankAccount is injected into the application service
            //  or you might be just instantiating it here
            bankAccount = repoBankAccount.findById(bankAccountId)

            bankAccount.Withdraw(amount)

            repoBankAccount.UpdateBankAccount(bankAccount)

            //You fire a domain event mentioning that the BankAccount has been updated
            messageBroker.Publish(new BankAccountUpdated(bankAccountId, amount, "WITHDRAW"))

            uow.commit()
        }

        public void CheckBalanceAmount(int bankAccountId)
        {
            // repoBankAccount is injected into the application service
            //  or you might be just instantiating it here
            bankAccount = repoBankAccount.findById(bankAccountId)

            return bankAccount.CheckBalance()
        }
    }

    public class TransactionService
    {
        public void AddNewTransaction(int transactionId, DateTime transactionDateTime, int amount, string transactionType)
        {
            Transaction new_transaction = new Transaction()
            {
                Id = transactionId,  // This is auto-generated
                TransactionDateTime = DateTime.Now,
                Amount = amount,
                TransactionType = transactionType
            };

            // repoTransaction is injected into the application service
            //  or you might be just instantiating it here
            repoTransaction.AddTransaction(new_transaction)
        }
    }
}

namespace ATM.ConsoleUICore
{
    class Program
    {
        static void Main()
        {
            var bankAccount = new BankAccount() { Id = 1, AccountName = "John", Balance = 250M };
            //repoBankAccount is injected here, or instantiated
            repoBankAccount.AddBankAccount(bankAccount)

            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 ${AccountService.CheckBalance(bankAccount.Id)}");
                    break;
                case "2":
                    // User to input amount.
                    // Data validation to make sure amount is greater than zero.

                    AccountService.DepositAmount(bankAccount.Id, 50);
                    Console.WriteLine("Deposit successfully");
                    break;
                case "3":
                    // User to input amount.
                    // Data validation to make sure amount is greater than zero.

                    AccountService.WithdrawAmount(bankAccount.Id, 50);
                    Console.WriteLine("Withdraw successfully");
                    break;
                default:
                    break;
            }

        }
    }
}

This should give sufficient clues on the direction to head in. I have not expanded on how exactly a message broker and DB would be initialized, assuming that you will be able to manage that.

If Transaction is a sub-entity of BankAccount, then you would do the same thing, except that persistence of Transaction is handled within AccountService, with the help of BankAccountRepository. You wouldn't have a separate TransactionRepository, nor will you need Domain Events for event propagation (There is no other aggregate to be updated).

You might think this amount of code for a simple requirement as yours is simply too much. That's one of the issues of trying to apply DDD to toy projects; they simply seem like too much to do. But the thing to remember is that this code can someday scale to a very robust application - easy to maintain and refactor and scale - as you grow to handle orders of magnitude of users.

  • 1
    Yes, that's correct. Console fires, say, DepositAmount and WithdrawAmount methods on the Application Service. App service, in turn, loads the bank account, calls Deposit and Withdraw appropriately on the bank account, persists it and returns Success/Failure to the Console. – Subhash Bhushan Aug 17 at 23:12
  • 1
    I have tried to expand on your code and added an example to my answer. Apologies, I am not well-versed with C#, but hopefully, this will give you sufficient direction. Let me know if you need more clarification on the aspects inside the code example. – Subhash Bhushan Aug 17 at 23:29
  • 1
    UoW is applicable within each Application Service method call. If we consider the scenario that Transaction is a sub-entity of BankAccount, there is a good chance that they are stored in different tables. You will use UoW to ensure both are committed together (ACID). – Subhash Bhushan Aug 18 at 8:22
  • 1
    But if BankAccount and Transaction are both aggregates, you should ideally NOT commit them as part of one transaction. One of the guidelines to keep in mind when creating Aggregates is that one client request only affects one Aggregate. If executing a command on one Aggregate instance requires that additional business rules run on one or more other Aggregates, use eventual consistency, through event processing, batch processing, or other update mechanisms, – Subhash Bhushan Aug 18 at 8:27
  • 1
    Updated example with Unit of Work usage - Check AccountService – Subhash Bhushan Aug 18 at 8:30
1
public void Deposit(int amount)
{
    // ...
    // repoTrans.AddTransaction(transaction);
}

This is the part of your code that is a bit weird, in terms of Domain Driven Design.

The domain model is usually composed of pure in-memory representations of your domain; they own their own data structures. So you either send them information so that they may modify themselves, or you ask them questions.

If transactions are part of the BankAccount; meaning that they are always stored and processed together, then you would normally see logic like

public void Deposit(int amount)
{
    // ...
    this.AddTransaction(transaction);
}

And the transaction would be written to your persistence store as part of the bank account (aka via the bank account "repository").

On the other hand, if transactions are a separate thing from Bank Accounts, then you are more likely to see code like

public Tranasaction Deposit(int amount)
{
    // ...
    return transaction;
}

Or, if the bank account really is just contributing information, then you might see something more like

Transaction transaction = new Transaction(int amount, Id<BankAccount> accountNumber);

As written in your example, we would expect the transaction to be part of the bank account itself -- it would not have its own repository, because it is saved as part of the BankAccount entity.

Broadly - the domain model doesn't know anything about persistence concerns like repositories or transactions. The domain model just implements the bookkeeping of the domain; the application layer is responsible for figuring out when to have information copied from transient storage to durable storage.

  • I think transaction is part of bank account. Each function like deposit, withdrawal or third party transfer, it will add a transaction record. – Steve Aug 18 at 6:42
1

I will only comment on the design and the code from the perspective of DDD. Specifically, using the free DDD Reference by Eric Evans, pp. 9, 10 and 17 (layered architecture and repositories).

According to Domain-Driven Design, domain model layer should not depend on any layer, right?

I think you incorrectly interpreting a statement like this:

Isolate the expression of the domain model and the business logic, and eliminate any dependency on infrastructure, user interface, or even application logic that is not business logic.

I think the author is mostly concerned about the risk that a lot of technical details of interaction with storage technologies leak into the domain layer.

The author then clarifies:

Concentrate all the code related to the domain model in one layer and isolate it from the user interface, application, and infrastructure code... The key goal here is isolation.

The goal of DDD is to

create a better software by focusing on the domain model rather than the technology.

The key obstacle in this context is:

When the domain‐related code is diffused through such a large amount of other code, it becomes extremely difficult to see and to reason about.

On the topic of repositories Evans suggests:

create a service that can provide the illusion of an in‐memory collection of all objects of that aggregate’s root type. Set up access through a well‐known global interface. Provide methods to add and remove objects, which will encapsulate the actual insertion or removal of data in the data store. Provide methods that select objects based on criteria meaningful to domain experts...

So putting it all together my conclusions are:

  • Your code is completely fine from DDD POV.

    1. You properly established a layered "architecture". Each layer depends only on the underlying ones.
    2. You clearly separated business logic from UI, persistence and infrastructure. (Though I am not sure, if your solution is practical: you need in the end to persist somehow the balance).
  • You should not go out of their way to pretend that persistence does not exist. Instead, you should focus on keeping the business logic clearly expressed, localized in domain layer and not interleaved with UI/infrastructure concerns. For that you can use Repository pattern to provide a simple persistence interface to domain layer. The challenge is to prevent each of the concerns (business logic and persistence) from leaking into the other domains.

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.