Skip to main content

Database Routing

Django-CFG provides automatic database routing based on app labels with zero manual .using() calls.

Automatic Routing

The DatabaseRouter automatically routes 100% of queries to the correct database based on app labelsβ€”no manual .using() calls needed!

How It Works​

The DatabaseRouter automatically routes database operations based on DATABASE_ROUTING_RULES:

# settings.py
DATABASE_ROUTERS = ['django_cfg.routing.DatabaseRouter']

DATABASE_ROUTING_RULES = {
'blog': 'blog_db',
'shop': 'shop_db',
# Apps not listed β†’ default database
}

Router Implementation​

The DatabaseRouter provides four key methods:

def db_for_read(self, model, **hints):
"""Route read operations to appropriate database."""
rules = getattr(settings, 'DATABASE_ROUTING_RULES', {})
app_label = model._meta.app_label

return rules.get(app_label, 'default')

Use Case: Routes SELECT queries to the correct database based on model's app_label.

Configuration​

Setup​

Advanced Feature

Most applications don't need database routing. Use this only if you have:

  • Multiple physical databases for different modules
  • Read replicas for performance
  • Separate analytics/reporting database
# api/config.py - ONLY use if you need multiple databases!
from django_cfg import DjangoConfig, DatabaseConfig
from .environment import env

class MyDjangoConfig(DjangoConfig):
# Single database (default for most SaaS apps)
databases = {
"default": DatabaseConfig.from_url(url=env.database.url),
}

# Generate Django settings
config = MyDjangoConfig()

For advanced multi-database setup (rare), see Multi-Database Guide.

Environment Configuration​

# .env or system environment variables
DATABASE__URL="postgresql://localhost/mydb"
# api/environment/loader.py
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict

class DatabaseConfig(BaseSettings):
url: str = Field(default="sqlite:///db/default.sqlite3")

model_config = SettingsConfigDict(
env_prefix="DATABASE__",
env_nested_delimiter="__",
)
Single Database for SaaS

Most SaaS applications use a single database with multiple apps/modules. Database routing is typically only needed for:

  • Read replicas (performance)
  • Analytics warehouses (separate reporting DB)
  • Legacy system integration

Advanced: Read Replica Setup​

For performance scaling with read replicas:

Configuration​

# api/environment/loader.py
class DatabaseConfig(BaseSettings):
url: str = Field(default="sqlite:///db/default.sqlite3")
url_replica: str | None = Field(default=None)

model_config = SettingsConfigDict(
env_prefix="DATABASE__",
env_nested_delimiter="__",
)
# Production ENV
DATABASE__URL="postgresql://app:pass@db-primary.example.com/mydb"
DATABASE__URL_REPLICA="postgresql://readonly:pass@db-replica.example.com/mydb"
# api/config.py
class MyDjangoConfig(DjangoConfig):
databases = {
"default": DatabaseConfig.from_url(url=env.database.url),
}

if env.database.url_replica:
databases["replica"] = DatabaseConfig.from_url(url=env.database.url_replica)
database_routers = ["myapp.routers.ReadReplicaRouter"]

Custom Router​

# myapp/routers.py
import random

class ReadReplicaRouter:
"""Route 80% of reads to replica, all writes to primary."""

def db_for_read(self, model, **hints):
"""Most reads go to replica."""
return 'replica' if random.random() < 0.8 else 'default'

def db_for_write(self, model, **hints):
"""All writes to primary."""
return 'default'

def allow_relation(self, obj1, obj2, **hints):
"""Allow relations between same database."""
return True

def allow_migrate(self, db, app_label, **hints):
"""Only migrate on primary."""
return db == 'default'

Usage Examples​

from apps.profiles.models import User

# Writes always go to primary
user = User.objects.create(email="user@example.com")
# β†’ Routed to 'default' (primary)

# Reads mostly go to replica (80%)
users = User.objects.all()
# β†’ Routed to 'replica' (80%) or 'default' (20%)

# Force specific database when needed
fresh_user = User.objects.using('default').get(id=user.id)
# β†’ Always reads from primary (latest data)

heavy_report = User.objects.using('analytics').aggregate(
total=Count('*'),
revenue=Sum('purchases__amount')
)
# β†’ Use analytics warehouse for heavy queries

Manual Database Selection​

# Force read from primary (when you need latest data)
latest_user = User.objects.using('default').get(id=123)

# Force read from replica (when eventual consistency is ok)
cached_users = User.objects.using('replica').filter(is_active=True)

# Use analytics database for reports
from django.db import connections

with connections['analytics'].cursor() as cursor:
cursor.execute("SELECT * FROM user_stats WHERE created_at > NOW() - INTERVAL '30 days'")
results = cursor.fetchall()

Routing Flow - Read Replica Pattern​

Flow Steps:

  1. User calls: User.objects.all()
  2. Django calls: router.db_for_read(User)
  3. Router randomizes: 80% replica, 20% primary
  4. Django executes query on selected database

Debugging Routing​

Check Which Database Will Be Used​

from django.db import router
from apps.blog.models import Post

# For reading
db = router.db_for_read(Post)
print(f"Post reads from: {db}") # blog_db

# For writing
db = router.db_for_write(Post)
print(f"Post writes to: {db}") # blog_db

# Check if migration is allowed
allowed = router.allow_migrate('blog_db', 'blog')
print(f"Migrate blog on blog_db: {allowed}") # True

View Active Router​

from django.conf import settings

print("Active routers:", settings.DATABASE_ROUTERS)
print("Routing rules:", settings.DATABASE_ROUTING_RULES)

Test Routing​

# Test automatic routing
post = Post.objects.create(title="Test")
print(post._state.db) # blog_db

user = User.objects.create_user(email="test@example.com")
print(user._state.db) # default

Advanced Patterns​

πŸ“š Read Replicas - Scale Read Operations

Read Replicas Setup​

# settings.py
DATABASE_ROUTING_RULES = {
'blog': 'blog_db',
'blog_replica': 'blog_replica_db',
}

Custom Router for Load Distribution​

class ReplicaRouter:
def db_for_read(self, model, **hints):
if model._meta.app_label == 'blog':
# 80% of reads go to replica
import random
return 'blog_replica_db' if random.random() < 0.8 else 'blog_db'
return None # Fall through to next router

def db_for_write(self, model, **hints):
if model._meta.app_label == 'blog':
return 'blog_db' # All writes to primary
return None

Enable Multi-Router Chain​

# settings.py
DATABASE_ROUTERS = [
'myapp.routers.ReplicaRouter', # Custom read replica logic
'django_cfg.routing.DatabaseRouter', # Fallback routing
]
Performance Gain

Read replicas can handle 80% of database load, reducing primary database pressure and improving response times.

πŸ”€ Sharding by User - Horizontal Scaling

User-Based Sharding​

def get_user_shard(user_id):
"""Determine database shard for user."""
shard_count = 4
shard_id = user_id % shard_count
return f"shard_{shard_id}"

# Use in queries
shard = get_user_shard(user.id)
user_posts = Post.objects.using(shard).filter(author=user)

Advanced Sharding Router​

class ShardingRouter:
def db_for_read(self, model, **hints):
if 'user_id' in hints:
return get_user_shard(hints['user_id'])
return None

def db_for_write(self, model, **hints):
if 'user_id' in hints:
return get_user_shard(hints['user_id'])
return None
Complexity Trade-off

Sharding adds complexity. Consider read replicas firstβ€”only shard when you exceed 100M+ records or 10K+ writes/sec.

Best Practices​

1. Use Automatic Routing​

# βœ… Good: Automatic routing
post = Post.objects.create(title="My Post")

# ❌ Bad: Manual routing (unless necessary)
post = Post.objects.using('blog_db').create(title="My Post")

2. Avoid Cross-Database JOINs​

# βœ… Good: Query from the correct side
orders = Order.objects.filter(user_id=user.id)

# ❌ Bad: Cross-database relationship traversal
orders = user.order_set.all() # May not work across databases

3. Document Routing Rules​

# settings.py
DATABASE_ROUTING_RULES = {
# Blog app β†’ blog_db (PostgreSQL on db-blog.example.com)
'blog': 'blog_db',

# Shop app β†’ shop_db (PostgreSQL on db-shop.example.com)
'shop': 'shop_db',

# Analytics app β†’ analytics (PostgreSQL read-only warehouse)
'analytics': 'analytics',
}

Troubleshooting​

Common Issue: Routing Not Working

Symptom: Queries still go to default database

Solution:

# 1. Check if router is configured
from django.conf import settings
print(settings.DATABASE_ROUTERS)
# Should show: ['django_cfg.routing.DatabaseRouter']

# 2. Check routing rules exist
print(settings.DATABASE_ROUTING_RULES)
# Should show: {'blog': 'blog_db', ...}

# 3. Verify app_label matches
from apps.blog.models import Post
print(Post._meta.app_label) # Should be 'blog', not 'apps.blog'
Wrong Database Being Used

Symptom: Model saves to unexpected database

Debug Steps:

# 1. Check which database is actually being used
from django.db import connections
print(connections['blog_db'].queries) # Show recent queries

# 2. Inspect model after save
post = Post.objects.create(title="Test")
print(post._state.db) # Should show 'blog_db'

# 3. Test router directly
from django.db import router
db = router.db_for_write(Post)
print(f"Router selected: {db}") # Should be 'blog_db'
Migration Issues - Quick Fix

Symptom: Migrations applied to wrong database

Solution:

# 1. Check migration status for all databases
python manage.py showmigrations

# 2. Migrate specific database
python manage.py migrate --database=blog_db

# 3. Use migrate_all for all databases (Django-CFG helper)
python manage.py migrate_all

# 4. Rollback if needed
python manage.py migrate blog 0003 --database=blog_db
πŸ” Advanced Debugging Techniques

Enable Query Logging​

# settings.py
LOGGING = {
'version': 1,
'handlers': {
'console': {'class': 'logging.StreamHandler'},
},
'loggers': {
'django.db.backends': {
'handlers': ['console'],
'level': 'DEBUG', # Shows all SQL queries with database alias
},
},
}

Test Routing Programmatically​

# test_routing.py
from django.test import TestCase
from django.db import router
from apps.blog.models import Post

class RoutingTestCase(TestCase):
def test_read_routing(self):
db = router.db_for_read(Post)
self.assertEqual(db, 'blog_db')

def test_write_routing(self):
db = router.db_for_write(Post)
self.assertEqual(db, 'blog_db')

def test_actual_database_used(self):
post = Post.objects.create(title="Test")
self.assertEqual(post._state.db, 'blog_db')

See Also​