a 2021 C# WinForms banking simulator that kept growing: deposits, withdrawals, transfers, bill payments, printable receipts, email alerts and an admin console.
ZEMO ATM is an old Windows Forms + SQL Server project built while I was learning how stateful desktop applications, database writes and multi-screen workflows fit together.
It is not real banking software and should not be treated as one. The useful part of keeping it public is the engineering story: a small ATM exercise gradually turned into a fairly large transaction-oriented desktop app, complete with all the architectural lessons that come with that.
These are the original screenshots from the project-era portfolio archive, now stored with this public repository.
There are two broad sides to the application.
A signed-in customer can:
- view the current account balance
- deposit money
- withdraw money
- use fixed-value fast-cash buttons
- transfer money to another simulated account
- browse recent transactions
- pay electricity bills
- pay water bills
- print payment receipts
- optionally receive email receipts
- enable/disable the historical e-receipt preference
- browse the bundled nearby-ATM directory
- change the account PIN
- receive a simulated security alert for large withdrawals
The separate management login opens an admin console for browsing and printing:
- account metadata
- recent transactions
- fund transfers
- bill payments
- e-receipt preferences
- security alerts
- nearby ATM records
The cleaned account view deliberately does not expose stored PIN/password values.
The original project changed balances and then wrote history records as separate SQL operations.
That is a dangerous shape for anything pretending to model financial state:
debit balance β
write transaction β
=> balance changed with no matching history
The current core flows move those related writes into SQL transactions.
begin transaction
β
βββ balance += amount
βββ insert RecentT
βββ read resulting balance
β
commit
begin serializable transaction
β
βββ UPDATE ... WHERE Balance >= amount
βββ insert RecentT
βββ read resulting balance
β
commit
The guarded update means the debit itself checks the available balance instead of trusting an earlier read.
This flow had the most important correctness bug in the original code.
Historically the sender was debited and the transfer was logged, but the recipient balance was never credited.
The current flow is:
begin serializable transaction
β
βββ verify recipient exists
βββ debit sender if funds are available
βββ credit recipient
βββ log sender transaction
βββ log recipient transaction
βββ insert FundTransfer record
β
commit
All of it succeeds together or rolls back together.
Electricity and water payments now also group the balance debit, recent-transaction entry and bill-payment record in one transaction.
Email/printing happens after the database transaction. A mail failure does not undo a successful local payment or leave its accounting half-written.
The project is still recognizably the old WinForms application, but the most sensitive shared behavior now has explicit boundaries.
WinForms screens
β
βββ BankingService
β βββ balances
β βββ deposits
β βββ withdrawals
β βββ transfers
β βββ bill payments
β βββ e-receipt state
β
βββ GridData
β βββ admin / lookup queries
β
βββ EmailService
β βββ optional SMTP + local HTML templates
β
βββ CredentialHasher
βββ PBKDF2-SHA256
β
βΌ
Database
β
βΌ
SQL Server / LocalDB
That is still far from how I would build a real banking system, but it removes a lot of duplicated database plumbing from button handlers and makes the important transaction boundaries visible.
The old version compared customer PINs and management passwords directly against plaintext database values.
Current sign-in does this instead:
account name
β
βΌ
parameterized lookup
β
βΌ
stored credential
β
βββ PBKDF2 value β verify hash
β
βββ old local plaintext value
β
βββ compare once
βββ successful login β replace with PBKDF2 hash
The compatibility branch is only there so an old development database can migrate after first login.
This is still not a production authentication design. A real banking product would not make a desktop client responsible for credential verification against a directly reachable database.
Database access now goes through one helper and can be configured with:
ZEMO_DB_CONNECTION_STRING
Without it, the project uses a local development database named ZEMO_Bank.
Queries touched by the overhaul use SQL parameters instead of concatenating user-controlled input into SQL strings.
The replacement Database.sql also fixes the historical schema problems:
- foreign keys point to the correct account table
- money tables have stable primary keys
- balances cannot be negative
- transfer senders and recipients are both account foreign keys
- self-transfers are rejected
- transaction-heavy tables have account/date indexes
- credential columns can hold hashes
- the admin account grid no longer needs to select credential fields
- the script contains only obvious disposable demo data
The old code embedded SMTP configuration directly in the desktop source and loaded templates from a developer-specific absolute Windows path.
That has been removed.
Email is now optional and configured at runtime:
ZEMO_SMTP_HOST
ZEMO_SMTP_PORT
ZEMO_SMTP_USERNAME
ZEMO_SMTP_PASSWORD
ZEMO_SMTP_FROM
Templates are loaded relative to the application directory:
index.html
TransactionAlert.html
ProfileAlert.html
If SMTP is not configured, the banking simulation still works.
For bill payments, the app falls back to the original printable DataGridView receipt flow when an email receipt cannot be sent.
One of the more specific product ideas in the original app was an e-receipt preference.
After enabling it, the UI intentionally prevented disabling it for 30 days.
That behavior is preserved because it is part of the original project rather than something I would invent in a cleanup.
The current code just moves the preference reads/writes out of raw button-handler SQL.
Withdrawals above the historical threshold create a SecurityAlerts record.
If SMTP is configured, the app also attempts to send the account email address an alert using the bundled transaction template.
The actual money movement does not depend on email delivery succeeding.
| Area | Technology |
|---|---|
| UI | C# Windows Forms |
| Runtime | .NET Framework 4.7.2 |
| Database | SQL Server / LocalDB |
| Data access | System.Data.SqlClient |
| Password/PIN storage | PBKDF2-SHA256 |
| Receipts | DataGridView + DGVPrinter |
| Optional mail | System.Net.Mail |
| Dependencies | NuGet / packages.config |
| Build | Visual Studio / MSBuild |
atm-system/
βββ ATM.sln
βββ Database.sql
βββ ATM/
β βββ Infrastructure/
β β βββ Database.cs
β β βββ GridData.cs
β βββ Security/
β β βββ CredentialHasher.cs
β βββ Services/
β β βββ BankingService.cs
β β βββ EmailService.cs
β βββ SignIn.cs
β βββ Dash.cs
β βββ Deposit.cs
β βββ Withdrawal.cs
β βββ FastCash.cs
β βββ FundTransfer.cs
β βββ BillPayment.cs
β βββ electricity.cs
β βββ water.cs
β βββ Settings.cs
β βββ ChangePIN.cs
β βββ NearbyATM.cs
β βββ Admin*.cs
β βββ index.html
β βββ TransactionAlert.html
β βββ ProfileAlert.html
βββ docs/
βββ engineering.md
βββ screenshots/
The .Designer.cs and .resx files are intentionally part of the repository. They are the actual WinForms layout/resources, not build output.
This is a Windows / .NET Framework project.
- Install Visual Studio with .NET desktop development and .NET Framework 4.7.2 targeting support.
- Restore NuGet packages for
ATM.sln. - Run
Database.sqlagainst SQL Server or LocalDB. - Set
ZEMO_DB_CONNECTION_STRINGif the default LocalDB setup is not suitable. - Open
ATM.slnand build/run the project.
Example PowerShell configuration:
$env:ZEMO_DB_CONNECTION_STRING = "Data Source=(LocalDB)\MSSQLLocalDB;Initial Catalog=ZEMO_Bank;Integrated Security=True"Optional SMTP setup:
$env:ZEMO_SMTP_HOST = "smtp.example.com"
$env:ZEMO_SMTP_PORT = "587"
$env:ZEMO_SMTP_USERNAME = "demo-user"
$env:ZEMO_SMTP_PASSWORD = "demo-password"
$env:ZEMO_SMTP_FROM = "demo@example.com"The development SQL script includes obvious disposable demo accounts. Their old-style local credentials are intentionally migrated to PBKDF2 by the current login path after a successful first sign-in.
The cleanup makes the repository safer to publish and easier to reason about. It does not turn the application into production banking infrastructure.
A real financial product would need, among other things:
- a trusted server-side API instead of a desktop client talking directly to SQL Server
- strong identity, MFA and session controls
- an immutable double-entry ledger rather than a mutable balance column as the primary source of truth
- idempotency for financial commands
- authorization at every server boundary
- audit/event retention
- encrypted secret management
- rate limiting and fraud controls
- deliberate money/currency types and rounding rules
- robust concurrency tests
- observability and recovery workflows
- compliance and security review far beyond a coursework project
The biggest change would be moving all trust off the desktop client.
desktop / web / mobile client
β
βΌ
authenticated backend API
β
βββ Accounts
βββ Transfers
βββ Payments
βββ Receipt delivery
βββ Admin authorization
β
βΌ
ledger + account read models
β
βΌ
SQL / event storage
The UI would submit commands like βtransfer 25.00 from A to Bβ; it would never receive database credentials or directly mutate balances.
I would also model money and transaction identifiers explicitly, make retries idempotent, use one signed-in-session abstraction instead of static fields, put email behind the server, and test all balance-changing operations independently from the WinForms UI.
There is a deeper breakdown in docs/engineering.md.
Because it shows a very different part of the journey from the mobile apps I build now.
It is messy in the useful way old projects are messy: lots of screens, lots of state, a database behind everything, several things I would never design the same way twice β and enough real behavior that revisiting it years later exposed interesting transaction, security and architecture problems worth fixing.
a 2021 desktop side quest; definitely not your bank.













