Back to blog
Backend Systemsbeginner

C# Project: Bank Account Simulator

Build a console-based bank account simulator in C# using OOP, LINQ, exception handling, and file persistence. A complete beginner project.

Asma HafeezApril 17, 20265 min read
csharpprojectooplinqconsole
Share:𝕏

C# Project: Bank Account Simulator

This project applies C# fundamentals: classes, records, LINQ, async/await, and file I/O. You'll build a working bank account system you can extend.


What We're Building

=== Bank Simulator ===
1. Create account
2. Deposit
3. Withdraw
4. Transfer
5. View balance
6. Transaction history
7. Save & Exit

> 1
Owner name: Alice
Account created: ACC-0001 (Alice) — Balance: $0.00

> 2
Account ID: ACC-0001
Amount: 1000
Deposited $1,000.00. New balance: $1,000.00

> 3
Account ID: ACC-0001
Amount: 250
Withdrawn $250.00. New balance: $750.00

Project Structure

BankSimulator/
├── Models/
│   ├── Account.cs
│   └── Transaction.cs
├── Services/
│   ├── Bank.cs
│   └── FileStorage.cs
└── Program.cs

Step 1: Models

C#
// Models/Transaction.cs
namespace BankSimulator.Models;

public enum TransactionType { Deposit, Withdrawal, Transfer }

public record Transaction(
    Guid Id,
    TransactionType Type,
    decimal Amount,
    decimal BalanceAfter,
    string Description,
    DateTimeOffset Timestamp
)
{
    public static Transaction Create(TransactionType type, decimal amount, decimal balanceAfter, string description)
        => new(Guid.NewGuid(), type, amount, balanceAfter, description, DateTimeOffset.UtcNow);

    public override string ToString()
        => $"{Timestamp:yyyy-MM-dd HH:mm} | {Type,-12} | {Amount,10:C} | Balance: {BalanceAfter:C} | {Description}";
}
C#
// Models/Account.cs
namespace BankSimulator.Models;

public class Account
{
    public string Id { get; }
    public string Owner { get; }
    public decimal Balance { get; private set; }
    public IReadOnlyList<Transaction> Transactions => _transactions.AsReadOnly();

    private readonly List<Transaction> _transactions = new();
    private static int _counter = 1;

    public Account(string owner)
    {
        if (string.IsNullOrWhiteSpace(owner))
            throw new ArgumentException("Owner name cannot be empty.");

        Owner = owner;
        Id = $"ACC-{_counter++:D4}";
    }

    public void Deposit(decimal amount, string? note = null)
    {
        if (amount <= 0) throw new ArgumentException("Deposit amount must be positive.");
        Balance += amount;
        _transactions.Add(Transaction.Create(
            TransactionType.Deposit, amount, Balance,
            note ?? "Deposit"));
    }

    public void Withdraw(decimal amount, string? note = null)
    {
        if (amount <= 0) throw new ArgumentException("Withdrawal amount must be positive.");
        if (amount > Balance) throw new InvalidOperationException(
            $"Insufficient funds. Balance: {Balance:C}, Requested: {amount:C}");
        Balance -= amount;
        _transactions.Add(Transaction.Create(
            TransactionType.Withdrawal, amount, Balance,
            note ?? "Withdrawal"));
    }

    public decimal TotalDeposits()
        => _transactions
            .Where(t => t.Type == TransactionType.Deposit)
            .Sum(t => t.Amount);

    public decimal TotalWithdrawals()
        => _transactions
            .Where(t => t.Type == TransactionType.Withdrawal)
            .Sum(t => t.Amount);

    public override string ToString()
        => $"{Id} ({Owner}) — Balance: {Balance:C}";
}

Step 2: Bank Service

C#
// Services/Bank.cs
namespace BankSimulator.Services;

using BankSimulator.Models;

public class Bank
{
    private readonly Dictionary<string, Account> _accounts = new();

    public Account CreateAccount(string owner)
    {
        var account = new Account(owner);
        _accounts[account.Id] = account;
        return account;
    }

    public Account GetAccount(string id)
    {
        if (!_accounts.TryGetValue(id, out var account))
            throw new KeyNotFoundException($"Account {id} not found.");
        return account;
    }

    public IEnumerable<Account> AllAccounts => _accounts.Values;

    public void Transfer(string fromId, string toId, decimal amount)
    {
        var from = GetAccount(fromId);
        var to   = GetAccount(toId);

        from.Withdraw(amount, $"Transfer to {toId}");
        to.Deposit(amount,   $"Transfer from {fromId}");
    }

    public IEnumerable<Account> AccountsSortedByBalance()
        => _accounts.Values.OrderByDescending(a => a.Balance);

    public decimal TotalDeposits()
        => _accounts.Values.Sum(a => a.TotalDeposits());
}

Step 3: File Storage

C#
// Services/FileStorage.cs
namespace BankSimulator.Services;

using System.Text.Json;
using BankSimulator.Models;

public class FileStorage
{
    private const string FilePath = "bank.json";

    public void Save(Bank bank)
    {
        var data = bank.AllAccounts.Select(a => new
        {
            a.Id,
            a.Owner,
            a.Balance,
            Transactions = a.Transactions.Select(t => new
            {
                t.Type,
                t.Amount,
                t.BalanceAfter,
                t.Description,
                Timestamp = t.Timestamp.ToString("O")
            })
        });

        string json = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
        File.WriteAllText(FilePath, json);
        Console.WriteLine($"Saved to {FilePath}");
    }
}

Step 4: Program.cs — Console UI

C#
// Program.cs
using BankSimulator.Services;

var bank = new Bank();
var storage = new FileStorage();

Console.WriteLine("=== Bank Simulator ===");

while (true)
{
    Console.WriteLine("""

    1. Create account
    2. Deposit
    3. Withdraw
    4. Transfer
    5. View balance
    6. Transaction history
    7. All accounts
    8. Save & Exit
    > """);

    var choice = Console.ReadLine()?.Trim();
    try
    {
        switch (choice)
        {
            case "1":
                Console.Write("Owner name: ");
                var owner = Console.ReadLine()!;
                var acc = bank.CreateAccount(owner);
                Console.WriteLine($"Account created: {acc}");
                break;

            case "2":
                var (acDep, amtDep) = PromptAccountAndAmount();
                acDep.Deposit(amtDep);
                Console.WriteLine($"Deposited {amtDep:C}. New balance: {acDep.Balance:C}");
                break;

            case "3":
                var (acWth, amtWth) = PromptAccountAndAmount();
                acWth.Withdraw(amtWth);
                Console.WriteLine($"Withdrawn {amtWth:C}. New balance: {acWth.Balance:C}");
                break;

            case "4":
                Console.Write("From account ID: ");
                var fromId = Console.ReadLine()!.Trim().ToUpper();
                Console.Write("To account ID: ");
                var toId = Console.ReadLine()!.Trim().ToUpper();
                Console.Write("Amount: ");
                var amount = decimal.Parse(Console.ReadLine()!);
                bank.Transfer(fromId, toId, amount);
                Console.WriteLine($"Transferred {amount:C} from {fromId} to {toId}");
                break;

            case "5":
                Console.Write("Account ID: ");
                var accId = Console.ReadLine()!.Trim().ToUpper();
                var viewAcc = bank.GetAccount(accId);
                Console.WriteLine($"Balance: {viewAcc.Balance:C}");
                Console.WriteLine($"  Deposits:    {viewAcc.TotalDeposits():C}");
                Console.WriteLine($"  Withdrawals: {viewAcc.TotalWithdrawals():C}");
                break;

            case "6":
                Console.Write("Account ID: ");
                var histId = Console.ReadLine()!.Trim().ToUpper();
                var histAcc = bank.GetAccount(histId);
                Console.WriteLine($"\n--- {histAcc} ---");
                foreach (var tx in histAcc.Transactions)
                    Console.WriteLine(tx);
                break;

            case "7":
                Console.WriteLine("\n--- All Accounts ---");
                foreach (var a in bank.AccountsSortedByBalance())
                    Console.WriteLine(a);
                Console.WriteLine($"Total deposits across all accounts: {bank.TotalDeposits():C}");
                break;

            case "8":
                storage.Save(bank);
                Console.WriteLine("Goodbye!");
                return;

            default:
                Console.WriteLine("Invalid option.");
                break;
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error: {ex.Message}");
    }
}

(Account, decimal) PromptAccountAndAmount()
{
    Console.Write("Account ID: ");
    var id = Console.ReadLine()!.Trim().ToUpper();
    Console.Write("Amount: ");
    var amount = decimal.Parse(Console.ReadLine()!);
    return (bank.GetAccount(id), amount);
}

Running the Project

Bash
dotnet new console -n BankSimulator
cd BankSimulator
# Create the folder structure and add the files above
dotnet run

What This Project Demonstrates

  • RecordsTransaction as an immutable value type
  • Classes with encapsulationAccount hides its balance modifications
  • Exception handling — informative errors for invalid operations
  • LINQWhere, Sum, OrderByDescending, Select
  • GenericsDictionary<string, Account>, List<Transaction>
  • Pattern matching — switch expressions with string patterns
  • JSON serializationSystem.Text.Json for persistence
  • Top-level statements — clean Program.cs with no boilerplate class

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.