University Payroll: 2,000 Employees
Calculate and freeze September payroll for 2,000 employees, process ten groups of 200, and trace one employee through bank confirmation and a salary slip.
Illustrative production scenario — fictional educational records
1. Business Scenario
The illustrative university employs 2,000 people. Each example employee has basic ₹50,000, HRA ₹20,000, allowance ₹10,000 and deductions ₹5,000. Gross is ₹80,000 and net is ₹75,000. At this uniform illustrative rate, total net payroll is ₹150,000,000 = ₹15 crore.
Payroll calculation and money movement are separate workflows. Freeze the approved salary snapshot so an edit to next month's salary structure cannot change this month's approved bank request.
Ten scheduling groups contain employees 1–200, 201–400, 401–600, 601–800, 801–1000, 1001–1200, 1201–1400, 1401–1600, 1601–1800 and 1801–2000. A scheduling group is not one atomic bank transaction.
| Reference | Amount per main request | Scope |
|---|---|---|
| SAL-2026-09-EMP0001 | ₹75,000 | External outcome plus local allocation |
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.
employees
| Column | Type / constraint |
|---|---|
| id | BIGINT PK |
| employee_no | VARCHAR(30) UNIQUE |
| name | VARCHAR(100) |
| department_id | BIGINT |
| bank_account_id | BIGINT FK private beneficiary store |
| employment_status | VARCHAR(20) |
Bank account numbers are masked in teaching screens and access-controlled in real systems.
salary_structures
| Column | Type / constraint |
|---|---|
| id | BIGINT PK |
| employee_id | BIGINT FK |
| basic_salary | DECIMAL(19,2) |
| hra | DECIMAL(19,2) |
| allowances | DECIMAL(19,2) |
| deductions | DECIMAL(19,2) |
| effective_from | DATE |
| effective_to | DATE NULL |
Select the structure effective for the payroll period; prevent overlapping effective ranges for an employee.
payroll_runs
| Column | Type / constraint |
|---|---|
| id | BIGINT PK |
| payroll_month | CHAR(7) |
| total_employees | INT |
| total_gross | DECIMAL(19,2) |
| total_deductions | DECIMAL(19,2) |
| total_net | DECIMAL(19,2) |
| status | VARCHAR(30) |
| created_by | BIGINT |
| approved_by | BIGINT NULL |
| approved_at | TIMESTAMP NULL |
Maker 11 and authorized checker 22 differ. Approval totals must equal the frozen item sums.
payroll_items
| Column | Type / constraint |
|---|---|
| id | BIGINT PK |
| payroll_run_id | BIGINT FK |
| employee_id | BIGINT FK |
| gross_salary | DECIMAL(19,2) |
| deductions | DECIMAL(19,2) |
| net_salary | DECIMAL(19,2) |
| status | VARCHAR(30) |
| payment_reference | VARCHAR(100) UNIQUE |
| failure_code | VARCHAR(100) NULL |
UNIQUE(payroll_run_id, employee_id). Store calculation snapshot/source version for audit.
bank_payment_requests
| Column | Type / constraint |
|---|---|
| id | BIGINT PK |
| payroll_item_id | BIGINT UNIQUE FK |
| business_reference | VARCHAR(100) UNIQUE |
| amount | DECIMAL(19,2) |
| status | VARCHAR(30) |
| attempt_count | INT |
| external_reference | VARCHAR(100) NULL |
| failure_code | VARCHAR(100) NULL |
Persist before sending. Bank submissions use the saved reference; do not derive a new key on timeout.
salary_slips
| Column | Type / constraint |
|---|---|
| id | BIGINT PK |
| payroll_item_id | BIGINT UNIQUE FK |
| slip_no | VARCHAR(60) UNIQUE |
| net_salary | DECIMAL(19,2) |
| status | VARCHAR(20) |
| file_path | VARCHAR(255) NULL |
Create after confirmed success; generated PDFs remain in private storage.
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 | employee_no | name | department_id | bank_account_id | employment_status |
|---|---|---|---|---|---|
| 1 | EMP0001 | Neha Verma | 10 | 301 | ACTIVE |
| 700 | EMP0700 | Arun Mehta | 20 | 370 | ACTIVE |
| id | employee_id | basic_salary | hra | allowances | deductions | effective_from | effective_to |
|---|---|---|---|---|---|---|---|
| 401 | 1 | 50000.00 | 20000.00 | 10000.00 | 5000.00 | 2026-09-01 | NULL |
| id | payroll_month | total_employees | total_gross | total_deductions | total_net | status | created_by | approved_by | approved_at |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 2026-09 | 2000 | 160000000.00 | 10000000.00 | 150000000.00 | PROCESSING | 11 | 22 | 2026-09-28 08:00:00 |
| id | payroll_run_id | employee_id | gross_salary | deductions | net_salary | status | payment_reference | failure_code |
|---|---|---|---|---|---|---|---|---|
| 20001 | 1 | 1 | 80000.00 | 5000.00 | 75000.00 | READY | SAL-2026-09-EMP0001 | NULL |
| 20700 | 1 | 700 | 80000.00 | 5000.00 | 75000.00 | READY | SAL-2026-09-EMP0700 | NULL |
| id | payroll_item_id | business_reference | amount | status | attempt_count | external_reference | failure_code |
|---|---|---|---|---|---|---|---|
| 30001 | 20001 | SAL-2026-09-EMP0001 | 75000.00 | READY | 0 | NULL | NULL |
| id | payroll_item_id | slip_no | net_salary | status | file_path |
|---|---|---|---|---|---|
| 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
- HR calculates September run
- Service writes frozen payroll_items
- Maker submits totals
- Checker authorizes run
- Outbox dispatches groups of 200
- Worker claims one item
- Bank adapter submits saved reference
- Callback/query confirms
- Slip and notification jobs
| Field | Value / rule |
|---|---|
| reference | SAL-2026-09-EMP0001 |
| amount | ₹75,000; validate against saved request |
| authenticated actor | PAYROLL-CHECKER-22 |
| response | Return saved result on duplicate; show pending while unresolved |
5. What Happens Inside the Application
The calculation service selects the salary structure valid for September, computes gross and net using decimal arithmetic, and writes 80000.00, 5000.00 and 75000.00 to payroll_items.id=20001. Run totals are sums of the persisted items, not a repeated estimate.
The approval service locks payroll_runs.id=1, verifies CALCULATED, checks maker/checker permissions and totals, then records approved_by/approved_at and durable outbox work. A second approval sees the new status and returns the existing result.
A worker atomically claims a READY request before contacting the bank outside the database transaction. A lease expiry or worker crash after a send creates uncertainty: reconcile the saved reference before another submission. The callback applies a terminal item outcome once; it does not reopen the historical payroll calculation.
Application responsibility boundaries
- Request
- Controller: authorize / validate
- Service: business rules / idempotency
- Database: locks / constraints / atomic writes
- Adapter: external protocol
- Callback / reconciler: verify evidence
- Outbox and document worker
6. Database Transaction
Transaction boundaries
- BEGIN: lock bank request 30001
- Claim READY → SUBMITTED with lease
- COMMIT before bank HTTP call
- Submit saved business reference
- Verify callback / status query
- BEGIN: lock request then payroll item
- Set confirmed SUCCESS; audit + slip 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.
Do not hold an invoice/account/item lock across bank network calls. Persist the intent first, commit it, then call the adapter. Apply verified results in a new short transaction. A database rollback cannot recall money already moved by an external bank.
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
-- Calculation snapshot before approval; bound values come from the effective salary structure.
START TRANSACTION;
SELECT id, status FROM payroll_runs WHERE id = 1 FOR UPDATE;
UPDATE payroll_items JOIN payroll_runs ON payroll_runs.id = payroll_items.payroll_run_id
SET payroll_items.gross_salary = 50000.00 + 20000.00 + 10000.00,
payroll_items.deductions = 5000.00, payroll_items.net_salary = 50000.00 + 20000.00 + 10000.00 - 5000.00
WHERE payroll_items.id = 20001 AND payroll_items.status = 'READY'
AND payroll_runs.status IN ('DRAFT','CALCULATED');
-- Recalculation is allowed ONLY while the parent run is DRAFT/CALCULATED and locked.
SELECT SUM(gross_salary), SUM(deductions), SUM(net_salary) FROM payroll_items WHERE payroll_run_id = 1;
COMMIT;
-- The guard prevents recalculating the PROCESSING sample run.
-- Separate transaction after a verified bank SUCCESS:
START TRANSACTION;
SELECT * FROM bank_payment_requests WHERE id = 30001 FOR UPDATE;
SELECT id, status FROM payroll_items WHERE id = 20001 FOR UPDATE;
-- If already SUCCESS, return existing result without creating a second slip.
UPDATE bank_payment_requests SET status = 'SUCCESS', external_reference = 'BANK-SAL-001' WHERE id = 30001;
UPDATE payroll_items SET status = 'SUCCESS' WHERE id = 20001;
INSERT INTO salary_slips (id, payroll_item_id, slip_no, net_salary, status)
VALUES (40001, 20001, 'SLIP-2026-09-0001', 75000.00, 'PENDING');
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.
8. Concurrent Request Scenario
| Time | Request / worker A | Request / worker B or external system |
|---|---|---|
| T0 | Worker A locks request 30001 | Worker B sees same queued item |
| T1 | Claim READY → SUBMITTED; COMMIT | Locks row, sees SUBMITTED; does not send |
| T2 | Bank request sent once | Can query status; cannot invent a retry key |
| T3 | Verified success callback locks request/item | Duplicate callback waits |
| T4 | Commit SUCCESS and slip event | Sees SUCCESS; acknowledge without second effect |
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 |
|---|---|
| gross_salary for item 20001 | 50000 + 20000 + 10000 = 80000 |
| deductions for item 20001 | 5000 |
| net_salary for item 20001 | 80000 - 5000 = 75000 |
| run 1 total_net | 2000 × 75000 = 150000000 (₹15 crore) |
| scheduling groups | 2000 / 200 = 10 |
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 |
|---|---|---|
| item 20001 net_salary | Uncalculated | 75000.00 frozen before approval |
| bank request 30001 | READY | SUCCESS / BANK-SAL-001 |
| item 20001 | READY | SUCCESS |
| item 20700 | READY | FAILED / ACCOUNT_CLOSED |
| run 1 | PROCESSING | PARTIALLY_COMPLETED after all 2000 outcomes are terminal |
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
- INITIATED / READY
- SUBMITTED
- PENDING / PROCESSING
- Verified SUCCESS
Failure / investigation branch
- SUBMITTED
- Timeout → UNKNOWN
- Query / callback / reconcile
- Verified SUCCESS or confirmed FAILED
Refund branch
- SUCCESS
- Authorized REFUND_REQUESTED
- Provider confirmation
- REFUNDED; retain original payment
| State | Meaning |
|---|---|
| READY / INITIATED | Saved intent; not proof of bank receipt |
| SUBMITTED | Send attempted; bank outcome not yet final |
| PENDING / PROCESSING | Provider confirms work in progress |
| UNKNOWN | No reliable external final outcome yet |
| SUCCESS | Verified final external success applied once locally |
| FAILED | Confirmed rejection; no blind assumption on timeout |
| REFUND_REQUESTED / REFUNDED | Separate authorized refund workflow with its own result |
Payroll run lifecycle
- DRAFT: editable
- CALCULATED: item totals ready
- APPROVED: snapshot frozen
- PROCESSING: individual bank requests
- COMPLETED: all success / PARTIALLY_COMPLETED: final mixed results
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.
Record verified provider/bank evidence before releasing a success notification. A merely submitted request stays pending or unknown until evidence resolves it.
Committed success to user
- Saved financial result
- COMMIT
- Outbox dispatcher
- Idempotent document job
- Private document ready
- Notification / authorized download
14. Failure Flow
Employee 700 returns ACCOUNT_CLOSED: mark only that request/item FAILED. The 1,999 successful payments remain successful. BANK_TIMEOUT is UNKNOWN, not a final failure, and prevents final closure until resolved. An approved payroll item is corrected through a separately audited adjustment, never silent recalculation.
| 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
For a timed-out SAL-2026-09-EMP0001, query the bank by that same reference before retrying. A confirmed pre-submission technical failure can retry safely under the same identity; a changed beneficiary after ACCOUNT_CLOSED requires an authorized correction linked to the original obligation.
Safe retry decision
- Find original saved reference
- Was any external request sent?
- Yes / uncertain → query original provider reference
- 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 | SAL-2026-09-EMP0001 | Apply once after validation/confirmation |
| Second | SAL-2026-09-EMP0001 | Return existing result / wait for original |
| Third | SAL-2026-09-EMP0001 | 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 |
|---|---|---|---|
| SAL-2026-09-EMP0001:document | SAL-2026-09-EMP0001 | 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)
GenerateSalarySlip::dispatch($payrollItem->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
SLIP-2026-09-0001 · Neha Verma / EMP0001 · September 2026 · Gross ₹80,000 · Deductions ₹5,000 · Net ₹75,000 · Bank reference BANK-SAL-001 · Payment SUCCESS. The PDF worker reads the frozen payroll item and does not recalculate from today's salary structure.
| Document field | Stored value |
|---|---|
| Salary slip | SLIP-2026-09-0001 |
| Employee | EMP0001 / Neha Verma |
| Period | September 2026 |
| Gross | INR 80000.00 |
| Deductions | INR 5000.00 |
| Net paid | INR 75000.00 |
| Bank reference | BANK-SAL-001 |
| Status | SUCCESS |
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
Expected net ₹150,000,000. With 1,999 confirmed successes, bank-paid total is 1999 × 75000 = ₹149,925,000; unpaid confirmed failure is ₹75,000. Match each reference to the bank report; distinguish authorized payroll liability from actual settled cash.
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 |
|---|---|---|---|
| SAL-2026-09-EMP0001 | UNKNOWN / missing confirmation | Verified provider/bank success | Resolved only after matching identity and amount |
Investigate outcomes, never blindly overwrite
-- Grouping is a monitoring signal; investigate individual references.
SELECT status, COUNT(*) AS rows_found, SUM(amount) AS amount_total
FROM bank_payment_requests
GROUP BY status;
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 |
|---|---|---|---|---|---|
| PAYROLL-CHECKER-22 | REQUEST_SAVED | SAL-2026-09-EMP0001 | NULL | READY | 2026-09-28 09:00:00 |
| VERIFIED-CALLBACK | RESULT_APPLIED | SAL-2026-09-EMP0001 | READY | SUCCESS | 2026-09-28 12:00:00 |
| DOCUMENT-WORKER | DOCUMENT_GENERATED | SAL-2026-09-EMP0001 | 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 | employees | total_net | success | failed | pending | status |
|---|---|---|---|---|---|---|
| 1 | 2000 | 150000000.00 | 1999 | 1 | 0 | PARTIALLY_COMPLETED |
| id | employee_id | net_salary | status | failure_code |
|---|---|---|---|---|
| 20001 | 1 | 75000.00 | SUCCESS | NULL |
| 20700 | 700 | 75000.00 | FAILED | ACCOUNT_CLOSED |
| slip_no | net_salary | status |
|---|---|---|
| SLIP-2026-09-0001 | 75000.00 | GENERATED |
| Financial result | Document | Outbox |
|---|---|---|
| SUCCESS | GENERATED after independent job | Published; worker deduplicates redelivery |
22. Interview Questions
Does employee 700's failure roll back everyone?
No. Each external salary payment has its own result. A database rollback cannot reverse earlier bank payments.
Why freeze the approved salary row?
Historical payroll, receipts and reconciliation must all use the exact amount approved for that month.
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.
