Transaction Schema, SQL & Laravel
Build the disposable account/request/ledger schema, load sample rows, and follow a Laravel transaction from input validation to final database state.
Illustrative production scenario — fictional educational records
1. Business Scenario
Implementation lab: map the withdrawal scenario to MySQL constraints and a Laravel service. Examples are source code for a disposable teaching database, never migrations for this job portal.
Account 101 belongs to the illustrative customer Rahul Sharma and holds ₹5,000. Request 7001 asks for ₹5,000; request 7002 asks for ₹10,000. Both arrive within milliseconds. They are separate authorized business requests, not duplicate deliveries.
The ₹10,000 request cannot succeed even if it acquires the lock first. In the illustrated order, request 7001 acquires the account lock first, commits its debit, and request 7002 then reads ₹0. The application must never decide from an earlier unlocked balance.
This lesson models an internal ledger withdrawal. Cash dispensing or an external bank debit would require an additional externally confirmed payment workflow; a local SUCCESS does not prove cash was delivered.
| Reference | Amount per main request | Scope |
|---|---|---|
| WD-7001 | ₹5,000 | One local database transaction |
2. Real Database Tables
These are educational schema definitions, not tables to add to the job portal's live database. PK means primary key; FK means a foreign key to the named parent. Use DECIMAL(19,2) for stored money and integer minor units or decimal-safe arithmetic in application code.
Use MySQL InnoDB for these row-locking examples. NOT NULL, foreign keys and scoped UNIQUE constraints complement application validation. Monetary CHECK constraints require an engine/version that enforces them; the service must still reject invalid amounts.
accounts
| Column | Type / constraint |
|---|---|
| id | BIGINT PK |
| account_no | VARCHAR(50) UNIQUE |
| customer_id | BIGINT NOT NULL |
| balance | DECIMAL(19,2) >= 0 |
| currency | CHAR(3) |
| status | VARCHAR(20) |
| updated_at | TIMESTAMP |
Use InnoDB. Authorize customer ownership; all amounts in this scenario are INR.
withdrawal_requests
| Column | Type / constraint |
|---|---|
| id | BIGINT PK |
| account_id | BIGINT FK accounts.id |
| amount | DECIMAL(19,2) > 0 |
| status | VARCHAR(30) |
| idempotency_key | VARCHAR(100) |
| failure_code | VARCHAR(100) NULL |
UNIQUE(account_id, idempotency_key). Reusing a key with a different amount is rejected.
account_transactions
| Column | Type / constraint |
|---|---|
| id | BIGINT PK |
| account_id | BIGINT FK |
| type | VARCHAR(30) |
| amount | DECIMAL(19,2) |
| reference_no | VARCHAR(100) UNIQUE |
| status | VARCHAR(30) |
No entry exists before the requests. The successful debit will create WD-7001. A full general ledger also has a balancing cash/clearing leg.
audit_logs
| Column | Type / constraint |
|---|---|
| id | BIGINT PK |
| user_id | BIGINT NULL (system actors allowed) |
| action | VARCHAR(60) |
| entity_type | VARCHAR(60) |
| entity_id | BIGINT |
| old_status | VARCHAR(30) NULL |
| new_status | VARCHAR(30) |
| metadata | JSON redacted |
| created_at | TIMESTAMP |
Append-only access policy; timestamps and actor identity explain who changed a result. Avoid full bank details or secrets in metadata.
outbox_events
| Column | Type / constraint |
|---|---|
| id | BIGINT PK |
| event_key | VARCHAR(150) UNIQUE |
| aggregate_reference | VARCHAR(100) |
| event_type | VARCHAR(60) |
| payload | JSON |
| created_at | TIMESTAMP |
| published_at | TIMESTAMP NULL |
Write in the financial transaction; a dispatcher retries unpublished events. Consumers also deduplicate because queue delivery may repeat.
3. Sample Table Data
The rows below are fictional starting records. NULL means no confirmed value is known. Empty ledger/document tables are intentional before processing; their inserted records appear in Final Database State.
Large-population examples show representative rows, not all 2,000 or 10,000 records. Run totals refer to the complete illustrative population, not the displayed subset.
| id | account_no | customer_id | balance | currency | status | updated_at |
|---|---|---|---|---|---|---|
| 101 | ACC-DEMO-101 | 101 | 5000.00 | INR | ACTIVE | 2026-09-28 09:00:00 |
| id | account_id | amount | status | idempotency_key | failure_code |
|---|---|---|---|---|---|
| 7001 | 101 | 5000.00 | PENDING | WD-CLIENT-001 | NULL |
| 7002 | 101 | 10000.00 | PENDING | WD-CLIENT-002 | NULL |
| id | account_id | type | amount | reference_no | status |
|---|---|---|---|---|---|
| No rows yet — this is the state before processing. | |||||
| event_key | published_at |
|---|---|
| No rows yet — this is the state before processing. | |
4. User Action / Request
The request is a business intent, not an instruction to trust browser-supplied status, owner or balance. The controller authorizes the actor, validates shape and currency, and resolves the saved obligation.
Button click to recorded result
- Customer presses Withdraw ₹5,000
- Controller authenticates account owner
- Create/reuse request 7001
- Withdrawal service locks request
- Lock account 101
- Validate and debit
- Commit ledger and outcome
- Queue receipt
| Field | Value / rule |
|---|---|
| reference | WD-7001 |
| amount | ₹5,000; validate against saved request |
| authenticated actor | CUSTOMER-101 |
| response | Return saved result on duplicate; show pending while unresolved |
5. What Happens Inside the Application
The controller validates a positive decimal amount and a client request key. It resolves account ownership from the authenticated session; it never accepts another customer's account merely because its ID is supplied.
The service loads the persisted request under a row lock. Its saved account_id and amount are authoritative. If a previous delivery already completed, return that result. Next lock the account, check ACTIVE and current funds, and perform a guarded debit.
Insert a uniquely referenced ledger entry and an audit event in the same transaction. Commit before acknowledging success. A duplicate-key or database exception rolls back the debit; it does not leave money removed without a ledger record.
Application responsibility boundaries
- Request
- Controller: authorize / validate
- Service: business rules / idempotency
- Database: locks / constraints / atomic writes
- Outbox and document worker
6. Database Transaction
Transaction boundaries
- BEGIN
- Lock withdrawal_requests 7001
- Check PENDING and stored amount
- Lock accounts 101
- Check ACTIVE and balance ≥ 5000
- Debit account and insert WD-7001
- Set request SUCCESS; add audit/outbox
- COMMIT
Atomicity applies to the writes enclosed by one BEGIN/COMMIT. Consistency means the amount, status, ledger/allocation and audit invariants agree. Isolation prevents conflicting decisions on locked rows. Durability means a committed result survives a client disconnect.
On unexpected database errors, roll back the local transaction. A known business rejection can instead be recorded and committed without a financial mutation. Never swallow a database exception and accidentally commit half of the intended changes.
All balance mutations here belong to the same local database transaction. External notifications are deferred until after commit; adding an external transfer changes the atomicity boundary.
7. SQL Query
SQL fragments show the successful branch of an application-controlled transaction. Comments describing checks are real control-flow requirements: the handler must stop/rollback or record the specified exception when a guard fails. Do not paste all branches as one unconditional money-moving script.
Representative SQL and guarded updates
START TRANSACTION;
SELECT id, account_id, amount, status FROM withdrawal_requests WHERE id = 7001 FOR UPDATE;
SELECT id, balance, status FROM accounts WHERE id = 101 FOR UPDATE;
-- Handler stops if request is already final, amount <= 0, or account inactive.
UPDATE accounts SET balance = balance - 5000.00
WHERE id = 101 AND status = 'ACTIVE' AND balance >= 5000.00;
SELECT ROW_COUNT() AS debited;
-- Continue ONLY if debited = 1; otherwise mark request FAILED, with no ledger insert.
INSERT INTO account_transactions (id, account_id, type, amount, reference_no, status)
VALUES (9001, 101, 'WITHDRAWAL', 5000.00, 'WD-7001', 'SUCCESS');
UPDATE withdrawal_requests SET status = 'SUCCESS' WHERE id = 7001;
-- Insert audit/outbox rows in this same transaction.
COMMIT;
Use prepared parameters in application code; literal IDs are used here only so learners can track the sample rows. Verify affected-row counts before the next financial write. Unique-reference violations require inspection of the existing saved outcome, not a new debit.
MySQL schema: accounts, requests and ledger
Teaching schema for a disposable MySQL 8.0.16+ InnoDB database. All example amounts use one currency. Production systems also need authorization, balanced ledger design, currency rules and bank integration controls. CHECK enforcement depends on the database version.
CREATE TABLE accounts (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
account_no VARCHAR(50) NOT NULL UNIQUE,
customer_id BIGINT NOT NULL,
balance DECIMAL(19,2) NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
CHECK (balance >= 0)
) ENGINE=InnoDB;
CREATE TABLE withdrawal_requests (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
account_id BIGINT NOT NULL,
amount DECIMAL(19,2) NOT NULL,
status VARCHAR(30) NOT NULL,
idempotency_key VARCHAR(100) NOT NULL,
failure_code VARCHAR(100) NULL,
UNIQUE KEY uq_withdrawal_idempotency (account_id, idempotency_key),
FOREIGN KEY (account_id) REFERENCES accounts(id),
CHECK (amount > 0)
) ENGINE=InnoDB;
CREATE TABLE account_transactions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
account_id BIGINT NOT NULL,
type VARCHAR(30) NOT NULL,
amount DECIMAL(19,2) NOT NULL,
status VARCHAR(30) NOT NULL,
reference_no VARCHAR(100) NOT NULL UNIQUE,
created_at TIMESTAMP NULL,
FOREIGN KEY (account_id) REFERENCES accounts(id),
CHECK (amount > 0)
) ENGINE=InnoDB;Add scoped unique constraints for invoice_no, payment_reference, gateway/provider reference, idempotency_key and batch_reference. Constraints protect against concurrent requests and application bugs.
Withdrawal SQL: check affected rows before ledger insertion
The application must validate the amount and lock the idempotent request first. This fragment shows the account operation; do not run the successful branch after a zero-row update.
START TRANSACTION;
SELECT id, balance, status FROM accounts WHERE id = 101 FOR UPDATE;
UPDATE accounts SET balance = balance - 5000
WHERE id = 101 AND status = 'ACTIVE' AND balance >= 5000;
SELECT ROW_COUNT() AS debited;
-- Application: if debited != 1, ROLLBACK and stop.
-- Only the debited = 1 branch continues:
INSERT INTO account_transactions
(account_id, type, amount, status, reference_no, created_at)
VALUES (101, 'WITHDRAWAL', 5000, 'SUCCESS', 'WD-10001', NOW());
COMMIT;If ledger insertion fails, roll back the debit. Repeated business references must return the recorded outcome rather than initiate a new debit.
Laravel: persisted amount, lock and guarded debit
Example assumes authenticated authorization for the request and validated decimal input when creating it. It performs a local ledger withdrawal, not an external bank payment. The persisted request supplies both amount and account.
DB::transaction(function () use ($requestId) {
$request = WithdrawalRequest::whereKey($requestId)
->lockForUpdate()->firstOrFail();
if ($request->status !== 'PENDING') return;
$account = Account::whereKey($request->account_id)
->lockForUpdate()->firstOrFail();
$amount = (string) $request->amount;
if (!preg_match('/^[0-9]+(?:\.[0-9]{1,2})?$/D', $amount)
|| !preg_match('/[1-9]/', $amount)) {
$request->update(['status' => 'FAILED', 'failure_code' => 'INVALID_AMOUNT']);
return;
}
if ($account->status !== 'ACTIVE') {
$request->update(['status' => 'FAILED', 'failure_code' => 'ACCOUNT_INACTIVE']);
return;
}
// Bound SQL arithmetic preserves DECIMAL precision.
$changed = DB::update(
'UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?',
[$amount, $account->id, $amount]
);
if ($changed !== 1) {
$request->update(['status' => 'FAILED', 'failure_code' => 'INSUFFICIENT_FUNDS']);
return;
}
AccountTransaction::create([
'account_id' => $account->id, 'type' => 'WITHDRAWAL',
'amount' => $amount, 'status' => 'SUCCESS',
'reference_no' => 'WD-'.$request->id,
]);
$request->update(['status' => 'SUCCESS']);
});Laravel: applying a verified student payment
Only run this flow after verifying the gateway event against the persisted payment intent, amount, currency and invoice. A durable inbox should retain the event for retries and reconciliation. Within DB::transaction: lock payment, return if SUCCESS, lock its invoice, atomically add the positive decimal amount to paid_amount and subtract from balance_amount with a balance >= amount guard, then set PAID or PARTIALLY_PAID and mark payment SUCCESS. Use bound decimal arithmetic as in the withdrawal example.
A zero-row invoice update after external success is an allocation exception, not proof of bank failure. Preserve the confirmed external payment for reconciliation or refund. Unique provider/reference constraints prevent duplicate callbacks from being credited repeatedly.
Laravel: payroll queues and receipts after commit
Approve a run only once under a row lock after checking the checker is authorized and different from the maker. Freeze totals and create durable outbox entries in the approval transaction. The outbox lets a dispatcher recover if the process crashes between commit and queue delivery.
For a simpler teaching example, Laravel supports afterCommit:
DB::transaction(function () use ($runId) {
$run = PayrollRun::whereKey($runId)->lockForUpdate()->firstOrFail();
if ($run->status !== 'CALCULATED') return;
// Authorization, maker/checker and frozen-total validation precede approval.
$run->update(['status' => 'APPROVED']);
$run->items()->orderBy('id')->chunkById(200, function ($items) {
foreach ($items as $item) {
$item->update(['status' => 'READY']);
ProcessSalaryPayment::dispatch($item->id)->afterCommit();
}
});
});
// In the transaction that records confirmed payment success:
GenerateFeeReceipt::dispatch($payment->id)->afterCommit();Workers must be idempotent. afterCommit prevents workers from seeing uncommitted data, but by itself is not a durable outbox. Never blindly resubmit UNKNOWN bank payments. Keep financial PDFs private and authorize every download.
HTTP/API -> Laravel -> MySQL -> Redis Queue -> Payment / Receipt / Notification / Reconciliation workers.
References: https://laravel.com/docs/10.x/queues#jobs-and-database-transactions and https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html
8. Concurrent Request Scenario
| Time | Request / worker A | Request / worker B or external system |
|---|---|---|
| T0 | BEGIN; lock request 7001 | BEGIN; lock request 7002 |
| T1 | Lock account 101; read 5000 | Attempt lock account 101; WAIT |
| T2 | Debit 5000; insert WD-7001 | Still waiting; cannot use a stale read |
| T3 | COMMIT; balance = 0 | Acquire lock; read NEW balance = 0 |
| T4 | Return SUCCESS | 0 < 10000; set FAILED / INSUFFICIENT_FUNDS; COMMIT |
A waiting worker must re-read the row after it obtains the lock. A value read before waiting cannot authorize a later debit/allocation. A second delivery of one business request differs from a genuinely new request: the former returns the same outcome, while the latter must recheck current funds or remaining obligation.
9. Locking / Isolation
SELECT ... FOR UPDATE is a locking read inside an explicit InnoDB transaction. The row remains protected until COMMIT or ROLLBACK. Use indexed primary/unique lookups so the operation does not scan and lock unrelated data.
Every handler touching multiple entities must follow the same lock hierarchy. Define it once for workers, callbacks and reconcilers; opposite order can create deadlocks. A deadlock victim must restart the whole local transaction and re-read data.
Lock the saved business request/payment first, then its mutable account/invoice/item. Keep the transaction short and use current locked values, even if an earlier page showed a different balance.
Lock lifetime
- BEGIN
- Indexed locking read
- Validate current row
- Guarded mutation
- COMMIT / ROLLBACK
- Lock released
10. Calculation
| Value | Calculation / expected result |
|---|---|
| Opening balance | 5000.00 |
| Successful debit | 5000.00 |
| Rejected debit (no effect) | 10000.00 |
| Closing balance | 5000.00 - 5000.00 = 0.00 |
Amounts shown in tables use two decimal places; commas and ₹ are display formatting, not database values. Round according to the documented currency/payroll rule once at the defined boundary. Do not add salary strings using floating-point arithmetic or silently change approved historical totals.
11. Table Update — Before / After
| Table or field | Before | After |
|---|---|---|
| accounts.balance | 5000.00 | 0.00 |
| withdrawal_requests 7001 | PENDING | SUCCESS |
| withdrawal_requests 7002 | PENDING | FAILED / INSUFFICIENT_FUNDS |
| account_transactions WD-7001 | Absent | 5000.00 WITHDRAWAL / SUCCESS |
The after rows describe only committed outcomes. A rollback preserves the before financial rows; a separately committed failure/audit record can still describe the rejected request. Background document status changes do not rewrite the financial success.
12. Status Transition
Success branch
- PENDING
- LOCK + VALIDATE
- SUCCESS
Failure / investigation branch
- PENDING
- Business validation fails
- FAILED; no debit
| State | Meaning |
|---|---|
| PENDING | Saved request awaits local execution |
| PROCESSING | Optional claimed state; not a final debit |
| SUCCESS | Committed local effects and ledger |
| FAILED | Confirmed validation/business rejection; no debit |
| REVERSED | Separate audited compensating entries; never erase original history |
13. Success Flow
Success means the defined financial operation and its associated local records are committed. Return the persisted reference and outcome to the client; do not generate a new success on each retry.
A successful response is sent after account/ledger/request writes commit together. If the response is lost, the next delivery reads the same committed success.
Committed success to user
- Saved financial result
- COMMIT
- Outbox dispatcher
- Idempotent document job
- Private document ready
- Notification / authorized download
14. Failure Flow
Insufficient funds is a confirmed business rejection: persist FAILED / INSUFFICIENT_FUNDS without a debit. An exception before COMMIT rolls back both debit and ledger. A deadlock victim may retry its database-only transaction; another client's completed withdrawal remains committed.
| Failure | Local action | Financial interpretation |
|---|---|---|
| Negative amount / invalid payload | REJECTED before submission | No financial operation |
| Insufficient funds / inactive entity | Record confirmed business failure | No local debit |
| Database error before external send | ROLLBACK | Retry only the rolled-back operation |
| Verified ACCOUNT_CLOSED | FAILED | Confirmed external rejection |
| BANK_TIMEOUT / lost response | UNKNOWN; investigate | May already have succeeded |
| PDF / email error | Retry document/notification only | Payment remains SUCCESS |
15. Retry Flow
A repeated client delivery uses WD-CLIENT-001 and returns request 7001's SUCCESS. Retry a rolled-back database attempt only with the same request and bounded attempts. Do not create a fresh request merely because the HTTP response was lost.
Safe retry decision
- Find original saved reference
- Was any external request sent?
- Only local transaction → inspect committed result
- Confirmed success → return; never repeat effect
- Confirmed safe failure → bounded authorized retry
- Exhausted / unresolved → investigation queue
Use bounded exponential backoff for transient technical errors and expose retry age/attempt count. An exhausted job becomes a failed/dead-letter item for an operator; a queue retry counter is not evidence of bank failure. A corrected obligation or authorized refund is a distinct linked business operation, not a stealth retry.
16. Idempotency
| Delivery | Business reference | Expected financial effect |
|---|---|---|
| First | WD-7001 | Apply once after validation/confirmation |
| Second | WD-7001 | Return existing result / wait for original |
| Third | WD-7001 | No additional debit, allocation or salary |
Persist a unique business key before processing and compare the request payload on key reuse. Apply another unique constraint to the external event/reference and to the document/outbox event. A unique key alone is not enough: the status check and financial mutation must be within the same transaction.
Callback authentication and idempotency solve different problems. Verify signatures/merchant context first; an attacker-controlled reference does not become trusted just because it is unique. Lock the request row before applying its result.
17. Queue / Background Processing
Durable background work
- Financial transaction inserts outbox event
- COMMIT
- Dispatcher reads unpublished events
- Queue accepts event
- Idempotent worker executes
- Mark completed / retry with same event key
| event_key | aggregate_reference | event_type | published_at |
|---|---|---|---|
| WD-7001:document | WD-7001 | GENERATE_DOCUMENT | NULL until dispatched |
Laravel afterCommit() prevents a worker from reading an uncommitted row, but a process can still crash between database commit and sending to a separate queue. A transactional outbox closes that publication gap; consumers must still handle redelivery.
Separate payment, document, notification and reconciliation queues allow independent retries and limits. Payroll/corporate schedulers paginate or chunk work rather than loading all 10,000 payments into one transaction. Do not submit external payments again just because their notification jobs failed.
Laravel dispatch after commit (illustrative job class)
GenerateWithdrawalReceipt::dispatch($request->id)->afterCommit();
// Job class is illustrative. Use a durable outbox for reliable publication.
// Deduplicate by the saved payment/request/document identity.
18. Receipt / Document Generation
Withdrawal acknowledgement WD-7001 shows ₹5,000, account ending 101, date 28 Sep 2026 and ledger status SUCCESS. It must not claim an external bank or ATM settlement that this example has not modeled.
| Document field | Stored value |
|---|---|
| Acknowledgement | WD-7001 |
| Account | Ending 101 |
| Amount | INR 5000.00 |
| Date | 28 Sep 2026 |
| Status | SUCCESS (local ledger only) |
Private document lifecycle
- Confirmed SUCCESS
- Database COMMIT
- Pending document / outbox
- Queue PDF renderer
- Private storage
- GENERATED
- Authorized download
| Payment | Document | Action |
|---|---|---|
| SUCCESS | PENDING | Wait for document worker |
| SUCCESS | FAILED_GENERATION | Retry renderer with same document key |
| SUCCESS | GENERATED | Reuse existing file; authorize each download |
Render from the committed immutable amount/reference and approved payroll snapshot, not client input. Reserve a unique receipt/slip number and link one document to one payment. A failed write to private storage cannot turn the already-confirmed financial payment into FAILED.
19. Reconciliation
Check opening balance 5000 minus the one successful debit 5000 equals closing balance 0. Request 7002 has no debit ledger entry. If an external dispenser exists, reconcile its journal separately; internal ledger success is insufficient evidence of dispensing.
Three-way evidence check
- Internal request / ledger / allocation
- Compare provider confirmation by reference
- Compare bank settlement by date/currency/net policy
- Create discrepancy record
- Investigate with evidence
- Apply idempotent authorized correction
- Close exception with audit
| reference | internal before | evidence | status / resolution |
|---|---|---|---|
| WD-7001 | Committed ledger | Matched request and balanced ledger | Resolved only after matching identity and amount |
Investigate outcomes, never blindly overwrite
SELECT account_id, type, SUM(amount) AS total
FROM account_transactions WHERE status = 'SUCCESS'
GROUP BY account_id, type;
Count mismatches are clues, not sufficient proof. Match identifiers, amounts, currency, beneficiary and dates. Handle settlement lag, fees and refunds explicitly; never change thousands of rows to SUCCESS merely to make dashboard totals agree.
20. Audit Log
| actor | action | entity | old_status | new_status | created_at |
|---|---|---|---|---|---|
| CUSTOMER-101 | REQUEST_SAVED | WD-7001 | NULL | PENDING | 2026-09-28 09:00:00 |
| TRANSACTION-SERVICE | RESULT_APPLIED | WD-7001 | PENDING | SUCCESS | 2026-09-28 12:00:00 |
| DOCUMENT-WORKER | DOCUMENT_GENERATED | WD-7001 | PENDING | GENERATED | 2026-09-28 12:01:00 |
Each financial status change appends an audit event inside the same local transaction. Metadata includes the request ID, correlation ID, provider evidence reference and reason, with secrets and full beneficiary details removed.
Restrict update/delete permissions on audit history and define retention. A manual reconciliation resolution records the authorized operator, evidence and correction reference. Audit explains who changed a value; the ledger explains the financial entries.
21. Final Database State
These tables describe the completed illustrative main scenario. Independent failure/overpayment exercises above do not silently alter these rows. Batch counts below are derived reporting values where marked.
| id | balance | currency | status |
|---|---|---|---|
| 101 | 0.00 | INR | ACTIVE |
| id | amount | status | failure_code |
|---|---|---|---|
| 7001 | 5000.00 | SUCCESS | NULL |
| 7002 | 10000.00 | FAILED | INSUFFICIENT_FUNDS |
| reference_no | account_id | amount | status |
|---|---|---|---|
| WD-7001 | 101 | 5000.00 | SUCCESS |
| Financial result | Document | Outbox |
|---|---|---|
| SUCCESS | GENERATED after independent job | Published; worker deduplicates redelivery |
22. Interview Questions
Could the ₹10,000 request win the race?
It may acquire the lock first but still fails: the starting balance is only ₹5,000.
Why lock the request as well as the account?
The request lock serializes duplicate deliveries; the account lock serializes distinct debits sharing one balance.
Why SELECT FOR UPDATE?
Protect a mutable row while the transaction makes and applies a decision from its current value.
What does a waiting request read?
The new committed value after obtaining its lock, not a cached value from before waiting.
Why cannot local rollback reverse a bank payment?
The bank is outside the local transaction boundary; use verified reconciliation and a separate authorized reversal/refund.
How are duplicate webhooks prevented from repeating money effects?
Authenticate the event, persist unique provider/business keys, lock the saved payment and apply the final result once.
What distinguishes FAILED, PENDING and UNKNOWN?
Confirmed rejection; confirmed work in progress; and unresolved external outcome, respectively.
How do 5,000 student payments or 10,000 salaries scale?
Persist independent obligations, use controlled idempotent workers, avoid long global locks, and reconcile each result.
Why queue receipt generation?
Document availability can retry independently without changing a confirmed financial outcome.
What should a deadlock retry repeat?
The entire rolled-back local transaction with its original business identity; never an uncertain external payment.
23. Related SQL Concepts
Connect the sample to ACID, row locks, isolation levels, foreign keys, UNIQUE constraints, conditional UPDATE, SUM/GROUP BY, joins, NULL external references and state transitions. Use window functions to review ordered attempts while preserving each payment's business identity.
