Every fintech team building P2P payments was reinventing the wheel—no standardized wallet logic, no atomic transfer guarantees, and balance inconsistencies under concurrent requests that could silently lose money.
A production-grade digital wallet REST API with atomic peer-to-peer transfers, signal-driven wallet provisioning, JWT authentication, and full transaction history—built on Django, DRF, and PostgreSQL.
High-level overview of the wallet and transfer flow.
Entity-Relationship diagram showing the wallet and transaction models.
Concurrent transfer requests could debit the sender without crediting the receiver, or allow double-spending when two requests hit the same wallet simultaneously.
Wrapped the entire transfer flow in transaction.atomic() with
select_for_update() row-level locking, ensuring both wallets are updated
as a single indivisible operation.
# Atomic P2P transfer with row-level locking
from django.db import transaction
def execute_transfer(sender, receiver_email, amount):
with transaction.atomic():
# Lock both wallets to prevent race conditions
sender_wallet = (
Wallet.objects
.select_for_update()
.get(user=sender)
)
receiver_wallet = (
Wallet.objects
.select_for_update()
.get(user__email=receiver_email)
)
if sender_wallet.balance < amount:
raise ValidationError("Insufficient funds")
sender_wallet.balance -= amount
receiver_wallet.balance += amount
sender_wallet.save()
receiver_wallet.save()
Transaction.objects.create(
sender=sender,
receiver=receiver_wallet.user,
amount=amount,
status="completed"
)
Manually creating wallets after user registration was error-prone—forgotten steps left users without wallets, breaking downstream transfer logic.
Used Django's post_save signal on the User model to automatically create a
wallet with a zero balance the instant a user registers—zero manual steps, zero
orphaned accounts.
# Signal-driven wallet creation
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=User)
def create_wallet(sender, instance, created, **kwargs):
"""Auto-provision a wallet for every new user."""
if created:
Wallet.objects.create(
user=instance,
balance=Decimal("0.00")
)
Without proper validation, users could transfer negative amounts (effectively stealing funds), send money to themselves, or overdraw their balance.
Implemented multi-layer validation at the serializer and model level: positive amount enforcement, self-transfer prevention, and balance sufficiency checks—all before the atomic transaction block executes.
# Multi-layer transfer validation
class TransferSerializer(serializers.Serializer):
recipient = serializers.EmailField()
amount = serializers.DecimalField(
max_digits=12, decimal_places=2,
min_value=Decimal("0.01") # No zero/negative transfers
)
def validate(self, data):
request = self.context["request"]
# Prevent self-transfers
if data["recipient"] == request.user.email:
raise serializers.ValidationError(
"Cannot transfer to yourself"
)
# Verify recipient exists
if not User.objects.filter(
email=data["recipient"]
).exists():
raise serializers.ValidationError(
"Recipient not found"
)
return data
Financial operations must be all-or-nothing. Django's
transaction.atomic() combined with select_for_update()
eliminates race conditions at the database level.
Using post_save signals for wallet creation keeps the registration flow
clean and guarantees every user gets a wallet—without touching the auth views.
Serializer-level checks catch malformed requests early. Model-level constraints act as the last line of defense. Never trust a single validation layer with money.
Clean endpoint design (/send/, /history/) with proper HTTP
methods and status codes makes the API intuitive for consumers and easy to document.
Check out the full source code or reach out to discuss the architecture.