Best way to work with a banking type class

Below is a quick mockup of what I would do as far as a starting point for interfaces and classes. In my mind, I savings account would typically have the same behaviour as a Current account so I would inherit that as a starting point to not rewrite, but in my example I left them completely separate.

I haven't including any samples for the repository itself.

C#:
    public enum AccountType
    {
        Savings = 1,
        Current = 2
    }

    public interface IAccount
    {
        public string AccountNumber { get; }

        public ICustomer Customer { get; }

        public decimal Balance { get; }

        public AccountType AccountType { get; }

        public void Open();
    }

    public interface ITransactionalAccount : IAccount
    {
        public decimal MinimumBalance { get; set; }

        public void Deposit(decimal amount);

        public void Withdraw(decimal amount);
    }

    public interface IInterestBearingAccount : IAccount
    {
        public decimal InterestRate { get; }
        public decimal CalculateInterest();
    }

    public class CurrentAccount : ITransactionalAccount
    {
        private IAccountRepository accountRepository;

        public string AccountNumber { get; protected set; }

        public ICustomer Customer { get; protected set; }

        public AccountType AccountType { get => AccountType.Current; }

        public decimal Balance { get; protected set; }

        public decimal MinimumBalance { get; set; }

        public CurrentAccount(IAccountRepository accountRepository, string accountNumber, ICustomer customer, decimal balance, decimal minimumBalance)
        {
            this.accountRepository = accountRepository;
            this.AccountNumber = accountNumber;
            this.Customer = customer;
            this.Balance = balance;
            this.MinimumBalance = minimumBalance;
        }

        public void Deposit(decimal amount)
        {
            this.Balance += amount;

            this.Update();
        }

        public void Withdraw(decimal amount)
        {
            decimal newBalance = this.Balance - amount;

            if (this.MinimumBalance < newBalance)
                throw new Exception("Insufficient funds");

            this.Balance = newBalance;

            this.Update();
        }

        private void Update()
        {
            accountRepository.Update(this);
        }

        public void Open()
        {
            this.AccountNumber = accountRepository.Create(this);
        }
    }

    public class SavingsAccount : ITransactionalAccount, IInterestBearingAccount
    {
        private IAccountRepository accountRepository;

        public string AccountNumber { get; protected set; }

        public ICustomer Customer { get; protected set; }

        public AccountType AccountType { get => AccountType.Current; }
        
        public decimal Balance { get; protected set; }

        public decimal MinimumBalance { get; set; }

        public decimal InterestRate { get; set; }

        public SavingsAccount(IAccountRepository accountRepository, string accountNumber, ICustomer customer, decimal balance, decimal minimumBalance, decimal interestRate)
        {
            this.accountRepository = accountRepository;
            this.AccountNumber = accountNumber;
            this.Customer = customer;
            this.Balance = balance;
            this.MinimumBalance = minimumBalance;
            this.InterestRate = interestRate;
        }

        public void Deposit(decimal amount)
        {
            this.Balance += amount;

            this.Update();
        }

        public void Withdraw(decimal amount)
        {
            decimal newBalance = this.Balance - amount;

            if (this.MinimumBalance < newBalance)
                throw new Exception("Insufficient funds");

            this.Balance = newBalance;

            this.Update();
        }

        private void Update()
        {
            accountRepository.Update(this);
        }

        public void Open()
        {
            this.AccountNumber = accountRepository.Create(this);
        }
          
        public decimal CalculateInterest()
        {
            // Calculate interest and return - just used as an example
        }

    }

    // AccountManager / AccountFactory for creating concrete implementations
    public class AccountManager
    {
        private IAccountRepository accountRepository;

        public AccountManager(IAccountRepository accountRepository)
        {
            this.accountRepository = accountRepository;
        }

        // The accountRepository Get method can call the factory / manager method
        public IAccount CreateAccount(AccountType accountType, string accountNumber, ICustomer customer, decimal balance, decimal? minimumBalance, decimal? interestRate)
        {
            switch(accountType)
            {
                case AccountType.Current:
                    return new CurrentAccount(accountRepository, accountNumber, customer, balance, minimumBalance.Value);
                case AccountType.Savings:
                    return new SavingsAccount(accountRepository, accountNumber, customer, balance, minimumBalance.Value, interestRate.Value);
                default:
                    throw new NotImplementedException();
            }
        }

        public IAccount CreateAccount(AccountType accountType, ICustomer customer, decimal? minimumBalance, decimal? interestRate)
        {
            switch(accountType)
            {
                case AccountType.Current:
                    return new CurrentAccount(accountRepository, null, customer, 0, minimumBalance.Value);
                case AccountType.Savings:
                    return new SavingsAccount(accountRepository, null, customer, 0, minimumBalance.Value, interestRate.Value);
                default:
                    throw new NotImplementedException();
            }
        }
    }

    public interface IAccountRepository
    {
        public string Create(IAccount account);

        public void Update(IAccount account);

        public IAccount Get(string accountNumber, ICustomer customer);
    }

    public interface ICustomer
    {
        public int CustomerId { get; }
    }

    public class Individual : ICustomer
    {
        public int CustomerId { get; internal set; }
    }

    public class Company : ICustomer
    {
        public int CustomerId { get; internal set; }
    }

There's a lot of questions I have about this design. Say if you are creating a new Savings Account you now have to pass in these parameters.

accountNumber, ICustomer customer, decimal balance, decimal minimumBalance, decimal interestRate

all from some external source whereas in mind balance, minimum balance, interest rate should be pulled from the database depending on the account type, customers balance etc.

The more I think about it the more complicated it gets and the reason is, I find it difficult to separate the Account from the Customer. The Account is an entity in itself which should have only one link to the client, the AccountNo.

Yet every time an account is updated, do you update the Customer too as part of the account's logic or do you have a whole separate Customer class and repository for handling customer crud. I just see confusion!! :confused:

Your thoughts would be appreciated!
 
Last edited:
There's a lot of questions I have about this design. Say if you are creating a new Savings Account you now have to pass in these parameters.

accountNumber, ICustomer customer, decimal balance, decimal minimumBalance, decimal interestRate

all from some external source whereas in mind balance, minimum balance, interest rate should be pulled from the database depending on the account type, customers balance etc.

The more I think about it the more complicated it gets and the reason is, I find it difficult to separate the Account from the Customer. The Account is an entity in itself which should have only one link to the client, the AccountNo.

Yet every time an account is updated, do you update the Customer too as part of the account's logic or do you have a whole separate Customer class and repository for handling customer crud. I just see confusion!! :confused:

Your thoughts would be appreciated!
Why would you update the customer? Ideally the account has a reference to the customer but unless you are specifically updating data relevant to the customer construct, you should just be doing crud on your account object.

In my mind yes, two classes, two interfaces and two repos. You may need to have a more "complex" create if you are reading from an external record and creating both the customer and the account at the same time, the first time an account is detected. That should be done by a store procedure, ideally with a transaction to cover both inserts.

If I'm understsnding you correctly, my approach would be to have a basic create for both classes individually which would handle independant creation of each object. I would have another method on the account repo that would be used to parse the external data stream ( could be data from a service, xml, rss, import whatever) into a tvp variable list and have a store procedure handle an insert/merge for both objects in the single transaction, all on the database side, keeping the load clean off your repo layer.

Not sure if others agree but that's how I'm handling third party data at the moment, works well for us.
 
There's a lot of questions I have about this design. Say if you are creating a new Savings Account you now have to pass in these parameters.

accountNumber, ICustomer customer, decimal balance, decimal minimumBalance, decimal interestRate

all from some external source whereas in mind balance, minimum balance, interest rate should be pulled from the database depending on the account type, customers balance etc.

The more I think about it the more complicated it gets and the reason is, I find it difficult to separate the Account from the Customer. The Account is an entity in itself which should have only one link to the client, the AccountNo.

Yet every time an account is updated, do you update the Customer too as part of the account's logic or do you have a whole separate Customer class and repository for handling customer crud. I just see confusion!! :confused:

Your thoughts would be appreciated!

I wouldn't update the Customer at the same time - you could - if you are using aggregate roots, but typically I would update them separately. In your table, the account record has a CustomerId field, but when you build your domain objects, you want the actual customer record and not just the Id.

You need to be careful of creating your domain objects to match your database tables and should rather focus on creating rich domain functionality and then sorting out the mappings in the data layer.

Have you put your solution up on GitHub yet?
 
You cannot have an account without being a customer.

Don't over complicate things.

Create the customer. Once that is done, you can then create an account for that customer.


Also, accounts are also not 1-1 with customers.

Take a home loan for instance. You can dual bond with say your partner. 2 customers can share the same home loan account.
 
Last edited:
You cannot have an account without being a customer.

Don't over complicate things.

Create the customer. Once that is done, you create can then create an account for that customer.


Also, accounts are also not 1-1 with customers.

Take a home loan for instance. You can dual bond with say your partner. 2 customers can share the same home loan account.
Multimapping is a good point.
 
Nitpicking
Code:
    public interface IAccountHelper
    {
        void Deposit(string accountId, decimal amountToDeposit);
        void OpenAccount(string accountId);
        void Withdraw(string accountId, decimal amountToWithdraw);
    }

You shouldn't be calling it a helper. Helper gives the idea of a static utility class. Personally I would call it IAccountManager or IAccountService.

Code:
        public string accountId{ get; set; }
        public string CustomerNumber { get; set; }
        public decimal TotalBalance { get; set; }

Your property names are not consistent. Stick to the conventions, it should be AccountID.

Whether it is Id or ID is up for discussion, but standard is that if an abbreviation's length is two characters or less it is all uppercase:

System.IO
System.Xml

Code:
SavingsAccountHelper accounthelper = new SavingsAccountHelper();

You should use your interface else what is the point.
Code:
//Interface name = new InterfaceImplementation();
IAccountHelper accountHelper = new SavingsAccountHelper();
 
Nitpicking
Code:
    public interface IAccountHelper
    {
        void Deposit(string accountId, decimal amountToDeposit);
        void OpenAccount(string accountId);
        void Withdraw(string accountId, decimal amountToWithdraw);
    }

You shouldn't be calling it a helper. Helper gives the idea of a static utility class. Personally I would call it IAccountManager or IAccountService.

Code:
        public string accountId{ get; set; }
        public string CustomerNumber { get; set; }
        public decimal TotalBalance { get; set; }

Your property names are not consistent. Stick to the conventions, it should be AccountID.

Whether it is Id or ID is up for discussion, but standard is that if an abbreviation's length is two characters or less it is all uppercase:

System.IO
System.Xml

Code:
SavingsAccountHelper accounthelper = new SavingsAccountHelper();

You should use your interface else what is the point.
Code:
//Interface name = new InterfaceImplementation();
IAccountHelper accountHelper = new SavingsAccountHelper();

Helper always scream static class to me. Regarding the naming standards\conventions, this is a nice guide.

https://www.dofactory.com/reference/csharp-coding-standards
 
I wouldn't update the Customer at the same time - you could - if you are using aggregate roots, but typically I would update them separately. In your table, the account record has a CustomerId field, but when you build your domain objects, you want the actual customer record and not just the Id.

You need to be careful of creating your domain objects to match your database tables and should rather focus on creating rich domain functionality and then sorting out the mappings in the data layer.

Have you put your solution up on GitHub yet?

Going to do it now. It's a mess now though as I tried to do it your way and got completely lost. Tried to add a repository and then got lost again. The idea of it is really awesome, however what gets me is the constructor parameters and also if you need a List<SavingsAccount> for example. I've stared at it for days and can't seem to figure out how to implement your design.

 
Last edited:
What on earth is the difference between Balance and Overdraft

Not sure why you need these complicated withdraw and deposit functions that calculate remainders/balances.


I gave the solution to the “correct” design of an account earlier in this thread - a list of credits and debits.

The balance is a computed field.
the ledger is immutable.
 
Is that a code question or in general?
Well in relation this this code it is both.

you don’t have a zero balance and a positive overdraft, or a positive balance and a zero overdraft.

You have a balance. That balance is positive, zero, or negative
 
Well in relation this this code it is both.

you don’t have a zero balance and a positive overdraft, or a positive balance and a zero overdraft.

You have a balance. That balance is positive, zero, or negative
Hmmm, my personal cheque account has both : available balance which is Overdraft + funds in account, balance of funds only which can be negative if going into Overdraft. Haven't checked how he's done it though.
 
Overdraft value: how far the balance can go below 0?
 
Hmmm, my personal cheque account has both : available balance which is Overdraft + funds in account, balance of funds only which can be negative if going into Overdraft. Haven't checked how he's done it though.

What you internet banking displays and what it stores are not 1:1.

having an overdraft limit of X would allow you to calculate the available amount based on balance.


bank accounts run on transactions. They are general ledgers.
 
Overdraft value: how far the balance can go below 0?

that would be a valid assumption.

However this design uses
Code:
public decimal OverDraftLimit { get; internal set; }
public decimal OverDraft { get; internal set; }


 
I should really stop drinking wine and doing code/design reviews :ROFL:
 
What you internet banking displays and what it stores are not 1:1.

having an overdraft limit of X would allow you to calculate the available amount based on balance.


bank accounts run on transactions. They are general ledgers.
oh 100% thats why I was asking code or in general :D
 
Top
Sign up to the MyBroadband newsletter
X