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
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.00Project Structure
BankSimulator/
├── Models/
│ ├── Account.cs
│ └── Transaction.cs
├── Services/
│ ├── Bank.cs
│ └── FileStorage.cs
└── Program.csStep 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 runWhat This Project Demonstrates
- Records —
Transactionas an immutable value type - Classes with encapsulation —
Accounthides its balance modifications - Exception handling — informative errors for invalid operations
- LINQ —
Where,Sum,OrderByDescending,Select - Generics —
Dictionary<string, Account>,List<Transaction> - Pattern matching — switch expressions with string patterns
- JSON serialization —
System.Text.Jsonfor persistence - Top-level statements — clean
Program.cswith no boilerplate class
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.