Back to Blog
PAPA Development Journal [Day 3/7] - Django Backend Architecture & Smart Split Algorithms
📋 Case Study

PAPA Development Journal [Day 3/7] - Django Backend Architecture & Smart Split Algorithms

B
Blake
Sep 23, 2025 By Blake 27 min read
As Blake Lab's cultural-technology integration experiment, Day 3 dives into Django backend's technical core, showcasing how elegant architecture design solves complex financial splitting needs for Indigenous communities.

🎯 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 savepoint for fine-grained transaction control

  • Amount 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

  1. Simple beats complex: Flat permission design is more maintainable than complex permission trees

  2. Business logic cohesion: Placing permission checks in Model layer improves code quality

  3. Query optimization is crucial: Correct prefetch strategies can improve performance 10x

  4. 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!

Enjoyed this article? Show some love!

0
Clap

Enjoyed this article?

Subscribe for engineering notes and AI development insights

We respect your privacy. No spam, unsubscribe anytime.

Share this article

Comments