Your system says the transaction succeeded. Your user says it didn't.
A user transfers money.
They see:
Transaction successful
They refresh their balance.
Nothing changed.
They refresh again.
Still nothing.
Now doubt sets in:
- Was I charged?
- Did it fail?
- Should I try again?
From the user's perspective, the system is broken.
From the system's perspective... everything is working perfectly.
Two realities, one system
Behind the scenes, your system already knows the truth:
- The transaction has been recorded
- The operation is valid
- The system state is correct
But what the user sees is different:
- The balance hasn't updated
- The UI reflects stale data
- The confirmation feels unreliable
You now have two realities:
1. The system truth (what actually happened)
2. The perceived truth (what the user sees)
And they are temporarily out of sync.
This is Eventual consistency
Modern systems are rarely fully synchronous.
To scale and remain resilient, they rely on:
- Asynchronous processing
- Distributed data stores
- Replication across services
Which leads to:
The system will become consistent... eventually
But that "eventually" is where problems begin.
What really happens under the hood
Let's break it down:
User initiates transaction
API writes to the primary database
An event is published
A background worker processes it
A read replica updates later
At any given moment:
- One part of the system says: "done"
- Another part says: "not yet"
Both are technically correct.
Correct systems are not enough. Users must perceive correctness.
Models
The models define the core domain of the system and illustrate how the backend maintains correctness while the user sees a delayed view.
# models.py
class Account(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
balance = models.DecimalField(max_digits=12, decimal_places=2, default=0)
class Transaction(models.Model):
account = models.ForeignKey(Account, on_delete=models.CASCADE)
amount = models.DecimalField(max_digits=10, decimal_places=2)
status = models.CharField(max_length=20, default="pending")
created_at = models.DateTimeField(auto_now_add=True)
class LedgerEntry(models.Model):
account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name="ledger_entries")
transaction = models.ForeignKey(Transaction, on_delete=models.CASCADE)
amount = models.DecimalField(max_digits=12, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
Account
- Represents a user's financial account.
-
balanceshows the current amount, but it may be temporarily outdated due to async processing.
Transaction
- Represents a single user-initiated transaction.
- Starts with status
pending, showing the action was received by the system. - Updated to
completedafter async processing finishes.
LedgerEntry
- Provides an immutable record of all transactions applied to an account.
- Ensures an auditable source of truth for balances.
- Even if the user's view is delayed, the ledger guarantees correctness.
- Reinforces eventual consistency and auditability.
Celery Task
This task handles the asynchronous processing, demonstrating how backend truth is updated safely while users perceive a delay.
# tasks.py (Celery)
from django.db import transaction
from celery import shared_task
from .models import Transaction, LedgerEntry
@shared_task
def process_transaction(transaction_id):
with transaction.atomic():
tx = Transaction.objects.select_for_update().get(id=transaction_id)
account = tx.account
LedgerEntry.objects.create(
account=account,
transaction=tx,
amount=tx.amount
)
account.balance += tx.amount
account.save()
tx.status = "completed"
tx.save()
process_transaction
- Runs asynchronously, updating the transaction and account balance.
- Uses
select_for_updatewithin a transaction to prevent race conditions when multiple transactions happen simultaneously. - Creates a
LedgerEntryto record the transaction. - Updates account
balanceand marks the transactioncompleted.
View
The view shows how the system immediately communicates to the user while the actual processing happens in the background.
# views.py
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from .models import Transaction, Account
from .tasks import process_transaction
def create_transaction(request):
account_id = request.POST.get("account_id")
amount = float(request.POST.get("amount"))
account = get_object_or_404(Account, id=account_id, user=request.user)
tx = Transaction.objects.create(account=account, amount=amount)
process_transaction.delay(tx.id)
return JsonResponse({
"transaction_id": tx.id,
"status": tx.status,
"message": "Transaction received and processing..."
})
create_transaction
- Receives
account_idandamountfrom the request. - Ensures the account belongs to the requesting user.
- Creates a
Transactioninpendingstate and triggers the async task. - Returns immediate feedback to the user.
Closing Thought
Eventual consistency ensures correctness in distributed systems. But trust is a UX problem.
- Show pending states
- Use optimistic UI
The system is correct. Users must believe it.

Top comments (0)