Requirements
Functional Requirements:
- users should be able to deposit cash to their bank account - from checking/savings etc
- users should be able to withdraw cash from their back accounts - checking/savings etc
- users should be able to check the balance on the checking/savings account
- users should be able to use atm cards from a different bank to withdraw cash
- the atm system should be able to monitor cash availability and notify the tellers automatically when running low on cash.
- have a withdrawal limit on the ATM where >2000$ on a given day is not authorized.
Non-Functional Requirements:
- should be able to use a ATM pin to verify authenticity
- prioritize consistency over availability - financial transactions depend on consistency in their data. If an ATM is unavailable the user can use an alternative ATM.
- should be able to handle 1000s of transactions every day
- should be able to handle transactions secure - in rest and in transit
- should have low latency - in the order of milliseconds to deposit/withdraw cash.
Capacity Estimation
Estimate the scale of the system. Consider daily active users, read/write ratio, storage requirements, bandwidth, and any relevant QPS calculations...
API Design
- PUT /depositMoney/{encrypted(cardNumber)} -> updatedBalance
- request:{amountDeposit: float accountType: 'checking'/'savings'}
- authentication token: token generated through PIN verification
- PUT /withdrawMoney/{encrypted(cardNumber)} -> updatedBalance
- request:{amountWithdrawal: float, accountType: 'checking'/'savings'}
- authentication token: token generated through PIN verification
- GET /viewBalance/{encrypted(cardNumber)} -> {accountType: {'checking'/'savings'}, accountBalance: float}
- authentication token: token generated through PIN verification
- POST /authenticateUser/{encrypted(cardNumber)} -> {status: boolean status, authenticationToken:"token"}
- request:{pin: hashed(pin)}
High-Level Design
- All our APIs reside behind an API gateway which does the load balancing/rate limiting etc.
- The service authenticateUser is used to authenticate a customer in our ATM system. It takes the encrypted(cardNumber) along with a a hashed pin, retrieves the Hash+salt(cardPin) for the given cardNumber from the postgres database card table, verifies the hashed and salted pin against the entered hashed pin with the stored salt and authenticates user. It generates an authentication token and stores in a redis cache with a short ttl to verify against for any subsequent api requests. Any request after the ttl of the authentication token will require a reauthentication.
- The service viewBalance is used to view the balance in the accounts for a given card. The api first ensures that the authentication token it has in hand is still valid against the redis cache and if it is valid, reads the postgres database the accounts present for the given card and retrieves the balance for the same from the account table and serves the client with the account types and the balance in each account.
- The service withdrawMoney is used to withdraw money from the accounts for a given card. The api ensures that the authentication token is valid, takes in the withdrawal amount and the account type as a part of the request object. It then verifies against the write database that the balance is sufficient for withdrawal, ensures against the dailyBalanceLimitReached that for the given card the balance limit has not reached and restricts to the limit. Then in a single atomic transaction in the write database, it updates the transaction table, the account table and updates the balance. A highly frequent CDC worker then syncs the read database. The write and read have to be separate paths as we do not want to overload the system with frequent reads leading to slow writes.
- The service depositMoney is used to deposit money to an account for a given card. The api ensures the authentication token is valid, takes in the deposit amount, calculates the sum, and in a single atomic transaction, updates the account table with balance and captures the transactions table. The CDC worker would then update the read database.
The read database needs to be partitioned by user id if the transactions through ATM are going to be frequent. If the volume of transactions are not going to be high, a sharding/partitioning strategy of state or county would be enough. Otherwise by user id would be a good strategy to scale the read volume in the system. The front end uses consistent hashing on the user ID to route each request to the correct shard with the ZooKeeper keeping a live registry of the available hosts. To verify that the request to authenticate/view etc comes from a real ATM we can use a certificate verification mechanism with the certificate in the client being randomized every 24 hours. Additionally we can use geofencing to verify the client is residing within a specific geographic boundary.
Database Design
- user - userId, userName, userPhone, userEmail, Age
- account - accountId, accountType,BankName, accountBalance, accountNumber
- transactions - transactionId, transactionCard, transactionType, accountId,UpdatedBalance, transaction time stamp
- card - cardId, Hash+salt(cardNumber), cardType, Hash+salt(cardPin), cardExpiration, accountNumber
- dailyBalanceLimitReached - cardId, cardType, timestamp, balanceLimitFlag
- ATMUsageActivity - cardId, cardActivity, cardActivityTimeStamp, cardAccountViewed, cardAccountBalance
- ATMSession - cardId, authenticationTime, checkoutTime
Detailed Component Design
- Since there is a tradeoff when using CDC from the write database, we could have some precomputed views of the balance and store them in a redis cache. The viewBalance would first check the redis cache and if the data is not available then use the read database. Redis is an in memory key value store and can use a combination of the hashedAccountNumber+accountType to store the balance. The first commit would be to SQL before cache update for the precomputed views.
- To comply with FDIC regulations all the financial data should be stored in logs to be auditable. To do this I first add another table in the data model capturing a particular user session. The kafka stream will have idempotency by session by the event timestamp. Additionally we will include watermarking within it to capture late arrival streams. This stream will then feed in to fraud analytics using a flink job. The event logs will then be stored in s3 storage with 6 month warm storage to address any disputes. Anything beyond that would be in cold storage.