Transactions Project is a wallet and ledger application built with React, Express, MongoDB, and Mongoose. The interesting part of the project is not just CRUD around wallets and transactions, but the way the backend tries to preserve balance correctness while multiple updates target the same wallet.
The backend uses an in-memory queue-like structure in server/models/transactions.js through the transactionsInProcess object.
How it works:
- Every incoming balance update is assigned a generated UUID.
- That UUID is pushed into a per-wallet array.
- Only the UUID at the head of the array is allowed to update the wallet balance.
- Other requests wait and keep checking until they become the first item in the queue.
Why this matters:
- Without ordering, two requests could read the same starting balance and both write conflicting results.
- This mechanism serializes balance updates for a single wallet, reducing the classic read-modify-write race condition.
- It keeps concurrency scoped per wallet, so unrelated wallets do not block each other.
In short, the project uses a lightweight FIFO coordination pattern so one wallet balance is updated in sequence instead of by overlapping writes.
The transaction flow is designed as a two-step consistency pattern:
- Create the transaction document.
- Apply the balance increment to the wallet.
If the balance update fails, the backend removes the newly created transaction entry.
Why this is useful:
- It avoids leaving an orphan transaction that claims money moved when the wallet balance did not actually change.
- It acts like a simple compensating rollback without requiring a full database transaction.
This is a practical consistency safeguard for a small Express + MongoDB application.
The app now treats email as the primary unique identity for a wallet.
Highlights:
POST /auth/registercreates a wallet only if the email does not already exist.POST /auth/loginchecks whether the email already exists.- The API sends a
nextActionhint (loginorregister) so the frontend can redirect users into the correct flow. - Email values are normalized to lowercase before lookup and storage.
This creates a cleaner real-world experience than forcing users to create a new wallet every time they want to access the dashboard.
Both wallet setup and transaction creation validate numeric precision and reject values with more than 4 decimal places.
Why that matters:
- It prevents accidental storage of overly precise floating-point values.
- It keeps money-related inputs predictable and constrained at the API boundary.
The transaction history is not fetched with a naive full collection scan.
The backend combines:
- a
walletIdindex, - a compound index on
walletId + date, - and an aggregation pipeline with
$match,$sort,$skip, and optional$limit.
Why this stands out:
- History is naturally wallet-scoped.
- Results come back newest-first.
- Pagination is already supported structurally.
The legacy POST /setup route still works, but internally it now delegates to the same registration logic as POST /auth/register.
That means:
- old integrations are less likely to break,
- while the new auth flow becomes the main path.
On database connection, the app attempts to:
- detect the old unique
nameindex, - remove it if present,
- and create the new unique
emailindex.
This is a small but valuable operational touch because it aligns old data structures with the new login model without requiring a separate manual setup step first.
The project uses express-validator to validate different request types centrally in server/validators/transactions.js.
This keeps controllers focused on business logic rather than repeating field validation everywhere.
In server/controllers/transactions.js, the controllers:
- normalize auth flow decisions,
- validate decimal precision,
- compute the post-transaction balance,
- derive transaction type (
CREDITorDEBIT), - and shape API responses consistently.
In server/models/transactions.js, the model layer contains the main business behavior:
- queue-based balance update coordination,
- wallet creation and lookup,
- transaction persistence,
- compensating delete on failure,
- and history retrieval using aggregation.
This separation makes the backend easier to reason about.
http://localhost:3001
-
POST /auth/login- Request:
{ email: "user@example.com" } - Response:
{ success, walletId, data, nextAction, errors }
- Request:
-
POST /auth/register- Request:
{ name: "User Name", email: "user@example.com", balance: 5000 } - Response:
{ success, walletId, nextAction, errors }
- Request:
-
POST /setup- Backward-compatible alias for registration
- Request:
{ name: "User Name", email: "user@example.com", balance: 5000 }
-
POST /transact/:walletId- Request:
{ amount: number, description: string } - Response:
{ success, data, errors }
- Request:
-
GET /transactions?walletId=<id>&skip=0&limit=<optional>- Response:
{ success, data, errors }
- Response:
GET /wallet/:id- Response:
{ success, data, errors }
- Response:
Fields:
_id: ObjectIdname: Stringemail: Stringbalance: NumbercreatedDate: Date
Indexes:
- default
_id - unique
email
Fields:
_id: ObjectIdwalletId: ObjectIdamount: Numberbalance: Numberdescription: Stringdate: DatetransactionType:CREDIT | DEBIT
Indexes:
- default
_id walletId- compound index:
walletId + date(desc) - compound index:
walletId + transactionType
- Open the
setupdirectory. - Run
backend-setup.cmd. - Run
frontend-setup.cmd.
- Open the
setupdirectory. - Run
start-server.cmd. - Run
start-website.cmd.
The strongest backend idea in this project is its wallet-level serialized balance update flow. Instead of allowing concurrent requests to freely mutate the same balance, the app coordinates updates per wallet, then adds rollback protection if the balance write fails. That makes this project more than a simple CRUD wallet demo—it is a small ledger system with deliberate safeguards around correctness.