C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
136: Building a Simple Banking Application
I want to start today with a snippet of code. Imagine you're building a banking app and you've just finished your first draft of the BankAccount class. It looks clean, it compiles, and when you test it with a few deposits, it seems to work perfectly.
public class BankAccount
{
public double Balance { get; set; }
public void Deposit(double amount)
{
Balance += amount;
}
public void Withdraw(double amount)
{
Balance -= amount;
}
}
At first glance, this is fine. But as a software engineer, I can tell you that this code is a ticking time bomb. If you put this into production, you'd be dealing with angry customers and accounting nightmares within a week.
The Danger of Floating Point Math and Unchecked Inputs
There are two major red flags here. First, the use of double. Most developers reach for double or float because they handle decimals, but these are binary floating-point types. They are designed for scientific calculations where an approximation is okay, but they are terrible for money. Over thousands of transactions, you'll end up with rounding errors—tiny fractions of a cent that vanish or appear out of nowhere.
Second, look at the Deposit method. What happens if a user (or a malicious actor) passes a negative number? Balance += -100.00 actually performs a withdrawal. You've accidentally created a backdoor where users can "deposit" their way into a negative balance without ever hitting your withdrawal logic.
Switching to Decimal and Adding Guard Clauses
To fix this, we need to use the decimal type. It's slower than a double, but it has much higher precision and is specifically designed for financial applications. We also need "guard clauses"—checks at the start of a method that stop execution if the input is invalid.
public class BankAccount
{
// We make the setter private so the balance can't be
// changed from outside the class (Encapsulation)
public decimal Balance { get; private set; }
public void Deposit(decimal amount)
{
if (amount <= 0)
{
throw new ArgumentException("Deposit amount must be positive.");
}
Balance += amount;
}
public void Withdraw(decimal amount)
{
if (amount <= 0)
{
throw new ArgumentException("Withdrawal amount must be positive.");
}
if (amount > Balance)
{
throw new InvalidOperationException("Insufficient funds.");
}
Balance -= amount;
}
}
Notice how I changed the Balance setter to private. You don't want some other part of your program to just say account.Balance = 1000000;. The only way the balance should ever change is through the logic we've defined in our methods.
Implementing a Transaction Ledger
A real bank doesn't just store a single number; they store a history. If a customer asks why their balance is $50, you can't just say "because the variable says so." You need a ledger. We can achieve this by adding a List<string> to track our movements.
public class BankAccount
{
public decimal Balance { get; private set; }
private readonly List<string> _transactions = new List<string>();
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("Amount must be positive.");
Balance += amount;
_transactions.Add($"Deposited {amount:C}. New Balance: {Balance:C}");
}
public void Withdraw(decimal amount)
{
if (amount <= 0) throw new ArgumentException("Amount must be positive.");
if (amount > Balance) throw new InvalidOperationException("Insufficient funds.");
Balance -= amount;
_transactions.Add($"Withdrew {amount:C}. New Balance: {Balance:C}");
}
public void PrintStatement()
{
Console.WriteLine("--- Account Statement ---");
foreach (var transaction in _transactions)
{
Console.WriteLine(transaction);
}
Console.WriteLine($"Final Balance: {Balance:C}");
}
}
I used the :C format specifier in the strings. This is a handy C# trick that automatically formats the number as currency based on the user's system locale (adding the $ or € symbol automatically).
Wiring it together in a Console Loop
To make this functional, you'll want a loop that lets the user interact with the account. I usually recommend a switch statement inside a while loop for this kind of simple CLI tool. It keeps the logic clean and easy to extend if you decide to add features like "Transfer" or "Close Account" later.
var account = new BankAccount();
bool running = true;
while (running)
{
Console.WriteLine("\n1. Deposit\n2. Withdraw\n3. Statement\n4. Exit");
var choice = Console.ReadLine();
try
{
switch (choice)
{
case "1":
Console.Write("Amount: ");
decimal dep = decimal.Parse(Console.ReadLine());
account.Deposit(dep);
break;
case "2":
Console.Write("Amount: ");
decimal wit = decimal.Parse(Console.ReadLine());
account.Withdraw(wit);
break;
case "3":
account.PrintStatement();
break;
case "4":
running = false;
break;
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
I wrapped the switch in a try-catch block. Since we are throwing exceptions in our BankAccount class for invalid amounts or insufficient funds, this is where we catch those errors and display them to the user without crashing the whole application.
📋 Practical Task
Exercise: Implementing Overdraft Protection and Interest Calculation
Your task is to extend the BankAccount class to make it more realistic. Modify the existing code to include the following features:
- Overdraft Limit: Add a property called
OverdraftLimit(decimal). Update theWithdrawmethod so that a user can withdraw more than their current balance, provided they do not exceed their overdraft limit (e.g., if balance is $10 and limit is $100, they can withdraw up to $110). - Interest Accrual: Create a method called
ApplyInterest(decimal rate). This method should calculate interest based on the current balance (Balance * rate) and deposit that amount into the account. The rate should be passed as a decimal (e.g., 0.05 for 5%). - Validation: Ensure that interest cannot be applied if the balance is currently negative (overdrawn).
Test your implementation by attempting to withdraw more than your balance (but less than your limit), and then applying interest to a positive balance.
There are no comments for now.