Designing a Concurrency-Safe ATM System in Modern C++

In this project, I built a simple, low-level ATM system with a concurrency twist using modern C++. The system is comprised of two core classes—Account and ATM—which work together to handle authentication, withdrawals, and deposits. Multiple threads (mimicking three users who share an account) access the same account concurrently and deposit funds simultaneously.
Introduction
I created this project as a hands-on exercise to deepen my understanding of modern C++ fundamentals. The design demonstrates key concepts such as RAII, smart pointers, move semantics, and thread safety, all integrated into a practical, object-oriented ATM simulation.
RAII and Resource Management
RAII (Resource Acquisition Is Initialization) ensures that resources are properly cleaned up when an object goes out of scope. For example, deleting a dynamically allocated object using the `delete` keyword. In my ATM system, both the Account and ATM classes leverage RAII wrappers offered in C++ - to automatically release resources like dynamically allocated memory and mutex locks.
Smart Pointers
Smart pointers are RAII wrappers that manage dynamically allocated memory for you. Smart pointers automatically deallocate memory (of the object they are pointing at) when the smart pointer goes out of scope. In modern C++, you have:
std::unique_ptr
for exclusive ownership—only one unique pointer can own an object.-
std::shared_ptr
for shared ownership—multiple pointers can refer to the same object, and the object is only deleted when the last pointer is gone.
In my design, I used std::shared_ptr
because the same Account must be accessed concurrently
by multiple threads.
Using a unique pointer wouldn’t work well here because it enforces exclusive ownership, and you can’t have three threads holding separate copies simultaneously. In addition, you cannot use move semantics and use `std::move` to transfer ownership of the pointer between threads because all threads have to have concurrent access to the object.
Adding raw pointers could work but they do not clean up after themselves unlike smart pointers. And simply having an array of Account objects instead of an array of pointers will incur a lot of copying overhead.
Move Semantics and the Rule of 5
Move semantics allow you to transfer ownership of an object's resources without the overhead of deep copying. By implementing a move constructor and move assignment operator, you can "steal" resources from temporary objects efficiently. This is particularly useful when returning objects by value or transferring them between containers.
In my Account class, I implemented the Rule of 5, which states that if your class manages resources, you should define (or delete) all of the following:
- Destructor
- Copy Constructor
- Copy Assignment Operator
- Move Constructor
- Move Assignment Operator
This approach ensures that resources are managed correctly regardless of whether the object is copied or moved, while modern compilers further optimize with copy elision (reduce copying that is not needed).
Thread Safety and Concurrency
In a concurrent environment, protecting shared data is essential. My design uses mutexes to ensure that operations like deposit and withdrawal are thread-safe, ensuring that even if multiple threads execute concurrently, the critical sections (like balance updates) are executed sequentially. I used `std::lock_guard` as an RAII wrapper around a `std::mutex` to ensure that the mutex is unlocked when it goes out of scope. I also used `std::scoped_lock` when creating my copy constructors to ensure both the calling object's and the other object's locks are acquired at the same time, gracefully.
For example, three threads can simultaneously deposit funds into the same account. Although the deposit function is executed concurrently, the mutex in the Account class serializes access to the balance update, preventing race conditions. In reality, the speed up of using multiple threads might be thwarted by the overhead of creating them and maintaining them. However, in larger projects where the critical sections are a smaller part, it can definitely cause improvements in speed.
Final Thoughts
This toy ATM project was an excellent opportunity to explore modern C++ concepts in a practical setting. By leveraging smart pointers, move semantics, RAII, and thread-safe designs, I built a robust system that handles concurrent operations gracefully. While the project is simple, the lessons learned here provide a strong foundation for tackling more complex, real-world applications.