🎯 Today's Core: From Community Financial Pain Points to Technical Solutions
After completing frontend UX design yesterday, today tackles the real technical challenge: How to design a backend architecture that handles complex split logic while maintaining high performance?
This isn't just about writing CRUD, but handling:
Time-sensitive splitting (late joiners don't split previous expenses)
Selective splitting (only split specific items)
Permission inheritance (group → event → expense permission propagation)
Real-time sync (conflict-free multi-user editing)
🏗️ Django Permission System: The Wisdom of Flat Design
Why Choose Flat Role Design?
After multiple iterations, I found complex permission trees actually confuse users. The final design uses flat + relational approach:
# Simplified but flexible permission model
class User(AbstractUser):
class UserRole(models.TextChoices):
ADMIN = 'ADMIN', 'System Administrator'
USER = 'USER', 'General User'
role = models.CharField(
"Role", max_length=20,
choices=UserRole.choices,
default=UserRole.USER
)
@property
def managed_groups(self):
"""Get managed groups through relationships"""
return self.managing_groups.all()
@property
def managed_events(self):
"""Get managed events through relationships"""
return Event.objects.filter(managers=self)
Design Philosophy: Permissions are not static roles, but dynamic relationships.
Business Logic Encapsulation Best Practices
class Event(models.Model):
def can_user_add_expense(self, user) -> bool:
"""
Layered permission checking logic
Demonstrates Indigenous kapot (age-grade) permission concepts
"""
if not user or not user.is_authenticated:
return False
# Tier 1: System admin - highest permission
if user.role == 'ADMIN':
return True
# Tier 2: Event locked - only managers can operate
if self.is_locked:
return self.can_user_manage(user)
# Tier 3: Active event - participants can add
if self.participants.filter(
user=user,
is_active=True
).exists():
return True
# Tier 4: Event managers - can operate even as non-participants
if self.managers.filter(id=user.id).exists():
return True
return False
Key Insight: Embedding permission logic directly in models, rather than scattering it in Views or Serializers, ensures business rule consistency.
🧮 Smart Split Algorithms: Handling Real-World Complexity
Unified Architecture for Four Split Modes
class ExpenseSplit(models.Model):
"""Expense split record - core algorithm carrier"""
class SplitType(models.TextChoices):
AVERAGE = 'AVERAGE', 'Average Split' # Common for family gatherings
RATIO = 'RATIO', 'Ratio Split' # Based on income ratio
FIXED = 'FIXED', 'Fixed Amount' # Pre-agreed amount
SELECTIVE = 'SELECTIVE', 'Selective' # Flexible choice
expense = models.ForeignKey(
Expense, on_delete=models.CASCADE,
related_name='splits'
)
participant = models.ForeignKey(
ActivityParticipant, on_delete=models.CASCADE
)
split_type = models.CharField(
max_length=20, choices=SplitType.choices
)
split_value = models.DecimalField(
max_digits=10, decimal_places=4,
help_text="Decimal for ratio, amount for fixed"
)
calculated_amount = models.DecimalField(
max_digits=10, decimal_places=2
)
# Audit fields
is_adjusted = models.BooleanField(default=False)
adjusted_by = models.ForeignKey(
User, null=True, on_delete=models.SET_NULL
)
adjusted_at = models.DateTimeField(null=True)
Innovative Implementation of Time-Sensitive Splitting
One of PAPA's most distinctive features:
def calculate_expense_splits(expense: Expense) -> List[ExpenseSplit]:
"""
Smart split calculation engine
Handles complex logic for time-sensitive and selective splitting
"""
splits = []
participants = expense.event.participants.filter(is_active=True)
for participant in participants:
# Time-sensitive check: special handling for late joiners
if participant.split_option == 'NO_SPLIT':
# Only split expenses after joining
if expense.date < participant.joined_at:
continue
# Selective splitting: whitelist mechanism
elif participant.split_option == 'PARTIAL_SPLIT':
# Check if in splitting whitelist
if expense.id not in participant.partial_split_expenses:
continue
# Calculate individual amount
amount = calculate_participant_amount(
expense.amount,
participant,
len(eligible_participants)
)
splits.append(ExpenseSplit(
expense=expense,
participant=participant,
split_type=participant.default_split_type,
calculated_amount=amount
))
return splits
Innovation: Incorporating time dimension into split logic, better matching real-world usage scenarios.
🗄️ PostgreSQL Query Optimization: From N+1 to 1
Problem: Initial N+1 Query Hell
# ❌ Anti-pattern: generates massive database queries
expenses = Expense.objects.all()
for expense in expenses:
print(expense.user.name) # N queries
print(expense.category.name) # N queries
for split in expense.splits.all(): # N queries
print(split.participant.user.name) # N*M queries
Solution: Smart Prefetch Strategy
# ✅ Optimized: only needs 3 queries
class ExpenseViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Expense.objects.select_related(
'user', # JOIN user table
'category', # JOIN category table
'event', # JOIN event table
'event__group' # JOIN group table
).prefetch_related(
'splits__participant__user', # Prefetch split data
Prefetch(
'event__participants',
queryset=ActivityParticipant.objects.select_related('user')
)
).annotate(
# Aggregate calculations, avoiding extra queries
total_splits=Count('splits'),
split_sum=Sum('splits__calculated_amount')
)
Performance Improvement: From average 200ms to 15ms (1000 records).
Precise Composite Index Design
class Meta:
indexes = [
# Composite index for common query paths
models.Index(
fields=['event', 'is_active', '-date'],
name='event_active_date_idx'
),
# Split query optimization
models.Index(
fields=['expense', 'participant'],
name='split_lookup_idx'
),
# Time range queries
models.Index(
fields=['date', 'event'],
condition=Q(is_active=True), # Partial index
name='active_date_event_idx'
),
]
🔄 Atomic Operations: Ensuring Data Consistency
Using Database Transactions for Complex Operations
from django.db import transaction
class ExpenseViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['post'])
def adjust_splits(self, request, pk=None):
"""
Adjust split method - requires atomicity guarantee
"""
expense = self.get_object()
with transaction.atomic():
# Create savepoint
sid = transaction.savepoint()
try:
# Delete old split records
expense.splits.all().delete()
# Create new split records
total_amount = Decimal('0')
for split_data in request.data['splits']:
split = ExpenseSplit.objects.create(
expense=expense,
**split_data
)
total_amount += split.calculated_amount
# Validate total amount
if abs(total_amount - expense.amount) > Decimal('0.01'):
transaction.savepoint_rollback(sid)
return Response(
{'error': 'Split total doesn\'t match expense amount'},
status=400
)
# Record audit log
ActivityLog.objects.create(
activity=expense.event,
action_type='SPLIT_ADJUST',
description=f'Adjusted expense split: {expense.description}',
operator=request.user,
metadata={
'expense_id': expense.id,
'old_count': old_count,
'new_count': len(request.data['splits'])
}
)
transaction.savepoint_commit(sid)
except Exception as e:
transaction.savepoint_rollback(sid)
raise e
return Response({'status': 'success'})
Key Technologies:
Use
savepointfor fine-grained transaction controlAmount validation prevents split errors
Complete audit trail tracking
🚀 Railway Deployment: Seamless Migration from Local to Cloud
Smart Environment Configuration
# settings/railway.py
import dj_database_url
from decouple import config, Csv
# Environment-aware configuration
ENV = config('ENV', default='development')
DEBUG = config('DEBUG', default=False, cast=bool)
# Smart database URL parsing
DATABASE_URL = config('DATABASE_URL', default='')
if DATABASE_URL:
# Railway special character handling
DATABASE_URL = DATABASE_URL.replace('{{', '').replace('}}', '')
DATABASES = {
'default': dj_database_url.parse(
DATABASE_URL,
conn_max_age=600, # Connection pool optimization
conn_health_checks=True, # Health checks
)
}
# Redis configuration with fallback mechanism
REDIS_URL = config('REDIS_URL', default=None)
if REDIS_URL:
# Production: Use Redis
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [REDIS_URL],
"capacity": 1500, # Message capacity
"expiry": 10, # Message expiry time
},
},
}
else:
# Development: Use in-memory
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer',
},
}
Deployment Script Best Practices
#!/bin/bash
# railway-predeploy.sh - Pre-deployment tasks
echo "🚀 Starting Railway deployment..."
# Database migration
python manage.py migrate --no-input
# Static file collection
python manage.py collectstatic --no-input
# Create default admin (if doesn't exist)
python manage.py shell -c "
from django.contrib.auth import get_user_model
User = get_user_model()
if not User.objects.filter(username='admin').exists():
User.objects.create_superuser('admin', '[email protected]', 'admin')
print('✅ Default admin created')
"
# Initialize test data (development only)
if [ "$ENV" = "development" ]; then
python create_test_scenarios.py
echo "✅ Test data initialized"
fi
echo "✅ Railway deployment ready!"
🎯 Today's Achievements and Technical Gains
Completed Technical Highlights
✅ Elegant implementation of flat permission system
✅ Innovative design of time-sensitive split algorithm
✅ PostgreSQL query optimization from 200ms to 15ms
✅ Atomic operations ensuring data consistency
✅ Seamless Railway deployment configuration
Key Technical Insights
Simple beats complex: Flat permission design is more maintainable than complex permission trees
Business logic cohesion: Placing permission checks in Model layer improves code quality
Query optimization is crucial: Correct prefetch strategies can improve performance 10x
Atomicity guarantees data integrity: Financial features must use transactions
Real Impact of AI-Assisted Development
GitHub Copilot helped generate 60% of serializer code
Claude 3.5 assisted in designing complex query optimization strategies
Core split algorithm logic still required human design (cultural understanding cannot be replaced by AI)
🔮 Tomorrow's Preview
Day 4 will share Frontend-Backend Integration Battle Stories, including:
Perfect coordination between React Query and Django REST
WebSocket real-time sync implementation details
Error handling and user experience optimization
End-to-end testing strategies
Technology's value lies in solving real problems, not showing off. PAPA's backend architecture proves: using the right technology to solve the right problems is the best design.
🚀 Blake Lab - Leading in Both AI and Cultural Technology
💡 20 years of government projects | 🤖 AI Indigenous applications | 🎯 Indigenous language NLP development | 📈 Cultural-sensitive design
🌐 wchung.tw | 📧 [email protected]
Follow Blake Lab to explore the infinite possibilities of technology and culture together!