+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 500 of 541

๐Ÿ“˜ Multi-tenancy: Database Patterns

Master multi-tenancy: database patterns in Python with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

Prerequisites

  • Basic understanding of programming concepts ๐Ÿ“
  • Python installation (3.8+) ๐Ÿ
  • VS Code or preferred IDE ๐Ÿ’ป

What you'll learn

  • Understand the concept fundamentals ๐ŸŽฏ
  • Apply the concept in real projects ๐Ÿ—๏ธ
  • Debug common issues ๐Ÿ›
  • Write clean, Pythonic code โœจ

๐ŸŽฏ Introduction

Welcome to this exciting tutorial on multi-tenancy database patterns! ๐ŸŽ‰ In this guide, weโ€™ll explore how to build applications that serve multiple clients (tenants) from a single database infrastructure.

Youโ€™ll discover how multi-tenancy can transform your Python applications into scalable, cost-effective solutions. Whether youโ€™re building SaaS applications ๐ŸŒ, enterprise software ๐Ÿข, or cloud platforms โ˜๏ธ, understanding multi-tenancy patterns is essential for writing robust, maintainable code.

By the end of this tutorial, youโ€™ll feel confident implementing multi-tenant architectures in your own projects! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Multi-tenancy

๐Ÿค” What is Multi-tenancy?

Multi-tenancy is like an apartment building ๐Ÿข. Think of it as a single building (application) that houses multiple families (tenants) who share common infrastructure (database, servers) while keeping their spaces (data) private and secure.

In Python terms, multi-tenancy allows a single application instance to serve multiple customers while ensuring data isolation and security. This means you can:

  • โœจ Serve multiple clients from one codebase
  • ๐Ÿš€ Scale efficiently by sharing resources
  • ๐Ÿ›ก๏ธ Keep each tenantโ€™s data completely isolated

๐Ÿ’ก Why Use Multi-tenancy?

Hereโ€™s why developers love multi-tenant architectures:

  1. Cost Efficiency ๐Ÿ’ฐ: Share infrastructure across tenants
  2. Easier Maintenance ๐Ÿ”ง: One codebase to update and deploy
  3. Scalability ๐Ÿ“ˆ: Add new tenants without new deployments
  4. Resource Optimization โšก: Better utilization of database connections

Real-world example: Imagine building a project management tool ๐Ÿ“Š. With multi-tenancy, you can serve hundreds of companies from the same application while keeping each companyโ€™s projects private!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Pattern 1: Shared Database, Shared Schema

Letโ€™s start with the simplest approach:

# ๐Ÿ‘‹ Hello, Multi-tenancy!
import sqlite3
from contextlib import contextmanager

class SharedSchemaDB:
    def __init__(self, db_path):
        self.db_path = db_path
        self._init_db()
    
    def _init_db(self):
        # ๐ŸŽจ Creating our shared schema
        with self._get_db() as conn:
            conn.execute('''
                CREATE TABLE IF NOT EXISTS tasks (
                    id INTEGER PRIMARY KEY,
                    tenant_id TEXT NOT NULL,  # ๐Ÿข Tenant identifier
                    title TEXT NOT NULL,
                    completed BOOLEAN DEFAULT 0
                )
            ''')
            conn.execute('CREATE INDEX IF NOT EXISTS idx_tenant ON tasks(tenant_id)')
    
    @contextmanager
    def _get_db(self):
        # ๐Ÿ”„ Database connection manager
        conn = sqlite3.connect(self.db_path)
        conn.row_factory = sqlite3.Row
        try:
            yield conn
            conn.commit()
        finally:
            conn.close()
    
    def add_task(self, tenant_id, title):
        # โž• Add task for specific tenant
        with self._get_db() as conn:
            conn.execute(
                'INSERT INTO tasks (tenant_id, title) VALUES (?, ?)',
                (tenant_id, title)
            )
        print(f"โœ… Added task for tenant {tenant_id}: {title}")

๐Ÿ’ก Explanation: Notice how we include tenant_id in every table! This ensures data isolation at the row level.

๐ŸŽฏ Pattern 2: Separate Schemas

Hereโ€™s a more isolated approach:

# ๐Ÿ—๏ธ Pattern 2: Each tenant gets their own schema
class SeparateSchemaDB:
    def __init__(self, db_path):
        self.db_path = db_path
        self.tenants = {}  # ๐Ÿ—‚๏ธ Track tenant schemas
    
    def create_tenant_schema(self, tenant_id):
        # ๐ŸŽจ Create isolated schema for tenant
        schema_name = f"tenant_{tenant_id}"
        
        with sqlite3.connect(self.db_path) as conn:
            # Create tenant-specific tables
            conn.execute(f'''
                CREATE TABLE IF NOT EXISTS {schema_name}_tasks (
                    id INTEGER PRIMARY KEY,
                    title TEXT NOT NULL,
                    completed BOOLEAN DEFAULT 0
                )
            ''')
        
        self.tenants[tenant_id] = schema_name
        print(f"๐Ÿข Created schema for tenant: {tenant_id}")
    
    def get_tenant_connection(self, tenant_id):
        # ๐Ÿ”Œ Get connection for specific tenant
        if tenant_id not in self.tenants:
            self.create_tenant_schema(tenant_id)
        return TenantConnection(self.db_path, self.tenants[tenant_id])

class TenantConnection:
    def __init__(self, db_path, schema_name):
        self.db_path = db_path
        self.schema_name = schema_name
    
    def add_task(self, title):
        # โž• Add task in tenant's schema
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                f'INSERT INTO {self.schema_name}_tasks (title) VALUES (?)',
                (title,)
            )
        print(f"โœจ Task added to {self.schema_name}!")

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: E-commerce Platform

Letโ€™s build a multi-tenant e-commerce system:

# ๐Ÿ›๏ธ Multi-tenant e-commerce platform
from datetime import datetime
import json

class MultiTenantStore:
    def __init__(self):
        self.db_path = "ecommerce.db"
        self._init_database()
    
    def _init_database(self):
        # ๐ŸŽจ Create our multi-tenant schema
        with sqlite3.connect(self.db_path) as conn:
            conn.execute('''
                CREATE TABLE IF NOT EXISTS stores (
                    id TEXT PRIMARY KEY,
                    name TEXT NOT NULL,
                    config TEXT,  # ๐ŸŽจ Store customization
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            ''')
            
            conn.execute('''
                CREATE TABLE IF NOT EXISTS products (
                    id INTEGER PRIMARY KEY,
                    store_id TEXT NOT NULL,
                    name TEXT NOT NULL,
                    price REAL NOT NULL,
                    stock INTEGER DEFAULT 0,
                    emoji TEXT,  # Every product needs an emoji! ๐ŸŽ
                    FOREIGN KEY (store_id) REFERENCES stores(id)
                )
            ''')
            
            conn.execute('''
                CREATE TABLE IF NOT EXISTS orders (
                    id INTEGER PRIMARY KEY,
                    store_id TEXT NOT NULL,
                    customer_email TEXT NOT NULL,
                    total REAL NOT NULL,
                    status TEXT DEFAULT 'pending',
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    FOREIGN KEY (store_id) REFERENCES stores(id)
                )
            ''')
    
    def create_store(self, store_id, name, config=None):
        # ๐Ÿช Create a new tenant (store)
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                'INSERT INTO stores (id, name, config) VALUES (?, ?, ?)',
                (store_id, name, json.dumps(config or {}))
            )
        print(f"๐ŸŽ‰ Welcome {name} to our platform!")
        return Store(self.db_path, store_id)

class Store:
    def __init__(self, db_path, store_id):
        self.db_path = db_path
        self.store_id = store_id
    
    def add_product(self, name, price, stock, emoji="๐Ÿ“ฆ"):
        # ๐Ÿ›๏ธ Add product to this store
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                '''INSERT INTO products (store_id, name, price, stock, emoji) 
                   VALUES (?, ?, ?, ?, ?)''',
                (self.store_id, name, price, stock, emoji)
            )
        print(f"{emoji} Added {name} to your store!")
    
    def create_order(self, customer_email, items):
        # ๐Ÿ›’ Create order for this store
        total = sum(item['price'] * item['quantity'] for item in items)
        
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                '''INSERT INTO orders (store_id, customer_email, total) 
                   VALUES (?, ?, ?)''',
                (self.store_id, customer_email, total)
            )
        
        print(f"๐Ÿ’ฐ New order! Total: ${total:.2f}")
        return total
    
    def get_analytics(self):
        # ๐Ÿ“Š Get store-specific analytics
        with sqlite3.connect(self.db_path) as conn:
            conn.row_factory = sqlite3.Row
            
            # Total revenue
            revenue = conn.execute(
                '''SELECT SUM(total) as revenue FROM orders 
                   WHERE store_id = ? AND status != 'cancelled' ''',
                (self.store_id,)
            ).fetchone()['revenue'] or 0
            
            # Product count
            products = conn.execute(
                'SELECT COUNT(*) as count FROM products WHERE store_id = ?',
                (self.store_id,)
            ).fetchone()['count']
            
            # Order count
            orders = conn.execute(
                'SELECT COUNT(*) as count FROM orders WHERE store_id = ?',
                (self.store_id,)
            ).fetchone()['count']
        
        print(f"๐Ÿ“Š Store Analytics:")
        print(f"  ๐Ÿ’ฐ Revenue: ${revenue:.2f}")
        print(f"  ๐Ÿ“ฆ Products: {products}")
        print(f"  ๐Ÿ›’ Orders: {orders}")
        
        return {'revenue': revenue, 'products': products, 'orders': orders}

# ๐ŸŽฎ Let's use it!
platform = MultiTenantStore()

# Create two different stores
tech_store = platform.create_store("tech_haven", "Tech Haven", {
    "theme": "dark",
    "currency": "USD"
})

book_store = platform.create_store("book_world", "Book World", {
    "theme": "light",
    "currency": "USD"
})

# Add products to each store
tech_store.add_product("Laptop", 999.99, 10, "๐Ÿ’ป")
tech_store.add_product("Mouse", 29.99, 50, "๐Ÿ–ฑ๏ธ")

book_store.add_product("Python Guide", 39.99, 25, "๐Ÿ“˜")
book_store.add_product("Coffee Table Book", 24.99, 15, "โ˜•")

๐ŸŽฏ Try it yourself: Add a feature to handle inventory updates when orders are placed!

๐ŸŽฎ Example 2: Multi-tenant Task Management

Letโ€™s make a scalable task management system:

# ๐Ÿ† Multi-tenant task management with advanced features
from enum import Enum
from typing import Dict, List, Optional
import hashlib

class IsolationLevel(Enum):
    ROW_LEVEL = "row"  # ๐Ÿ”„ Shared tables with tenant_id
    SCHEMA_LEVEL = "schema"  # ๐Ÿ—๏ธ Separate schemas
    DATABASE_LEVEL = "database"  # ๐Ÿข Separate databases

class TenantManager:
    def __init__(self, isolation_level: IsolationLevel):
        self.isolation_level = isolation_level
        self.tenants: Dict[str, 'Tenant'] = {}
        self._init_system()
    
    def _init_system(self):
        # ๐ŸŽจ Initialize based on isolation level
        if self.isolation_level == IsolationLevel.ROW_LEVEL:
            print("๐Ÿ”„ Using row-level isolation")
            self.db = RowLevelDB()
        elif self.isolation_level == IsolationLevel.SCHEMA_LEVEL:
            print("๐Ÿ—๏ธ Using schema-level isolation")
            self.db = SchemaLevelDB()
        else:
            print("๐Ÿข Using database-level isolation")
            self.db = DatabaseLevelDB()
    
    def create_tenant(self, tenant_id: str, name: str, plan: str = "free"):
        # ๐ŸŽฏ Create new tenant
        tenant = Tenant(tenant_id, name, plan, self.db)
        self.tenants[tenant_id] = tenant
        
        # Initialize tenant space
        self.db.initialize_tenant(tenant_id)
        
        print(f"๐ŸŽ‰ Welcome {name}! Plan: {plan} ๐Ÿš€")
        return tenant

class Tenant:
    def __init__(self, tenant_id: str, name: str, plan: str, db):
        self.tenant_id = tenant_id
        self.name = name
        self.plan = plan
        self.db = db
        self.users: Dict[str, 'User'] = {}
    
    def add_user(self, email: str, name: str, role: str = "member"):
        # ๐Ÿ‘ค Add user to tenant
        user_id = hashlib.md5(f"{self.tenant_id}:{email}".encode()).hexdigest()[:8]
        user = User(user_id, email, name, role, self.tenant_id)
        self.users[user_id] = user
        
        self.db.add_user(self.tenant_id, user)
        print(f"โœ… Added {name} ({role}) to {self.name}")
        return user
    
    def get_usage_stats(self):
        # ๐Ÿ“Š Get tenant usage statistics
        stats = self.db.get_tenant_stats(self.tenant_id)
        
        # Apply plan limits
        limits = {
            "free": {"users": 5, "tasks": 100},
            "pro": {"users": 50, "tasks": 10000},
            "enterprise": {"users": -1, "tasks": -1}  # Unlimited
        }
        
        plan_limits = limits.get(self.plan, limits["free"])
        
        print(f"๐Ÿ“Š Usage Stats for {self.name}:")
        print(f"  ๐Ÿ‘ฅ Users: {stats['users']}/{plan_limits['users'] if plan_limits['users'] > 0 else 'โˆž'}")
        print(f"  ๐Ÿ“ Tasks: {stats['tasks']}/{plan_limits['tasks'] if plan_limits['tasks'] > 0 else 'โˆž'}")
        
        return stats, plan_limits

class User:
    def __init__(self, user_id: str, email: str, name: str, role: str, tenant_id: str):
        self.user_id = user_id
        self.email = email
        self.name = name
        self.role = role
        self.tenant_id = tenant_id

# ๐Ÿ”ง Database implementations
class RowLevelDB:
    def __init__(self):
        self.conn = sqlite3.connect(":memory:")
        self._create_tables()
    
    def _create_tables(self):
        # ๐Ÿ—๏ธ Create shared tables with tenant_id
        self.conn.execute('''
            CREATE TABLE tenants (
                id TEXT PRIMARY KEY,
                name TEXT NOT NULL,
                plan TEXT DEFAULT 'free',
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        ''')
        
        self.conn.execute('''
            CREATE TABLE users (
                id TEXT PRIMARY KEY,
                tenant_id TEXT NOT NULL,
                email TEXT NOT NULL,
                name TEXT NOT NULL,
                role TEXT DEFAULT 'member',
                FOREIGN KEY (tenant_id) REFERENCES tenants(id),
                UNIQUE(tenant_id, email)
            )
        ''')
        
        self.conn.execute('''
            CREATE TABLE tasks (
                id INTEGER PRIMARY KEY,
                tenant_id TEXT NOT NULL,
                user_id TEXT NOT NULL,
                title TEXT NOT NULL,
                completed BOOLEAN DEFAULT 0,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (tenant_id) REFERENCES tenants(id),
                FOREIGN KEY (user_id) REFERENCES users(id)
            )
        ''')
        
        # ๐Ÿš€ Create indexes for performance
        self.conn.execute('CREATE INDEX idx_users_tenant ON users(tenant_id)')
        self.conn.execute('CREATE INDEX idx_tasks_tenant ON tasks(tenant_id)')
    
    def initialize_tenant(self, tenant_id: str):
        # ๐ŸŽฏ Initialize tenant in shared tables
        pass  # No special initialization needed for row-level
    
    def add_user(self, tenant_id: str, user: User):
        # ๐Ÿ‘ค Add user with tenant isolation
        self.conn.execute(
            '''INSERT INTO users (id, tenant_id, email, name, role) 
               VALUES (?, ?, ?, ?, ?)''',
            (user.user_id, tenant_id, user.email, user.name, user.role)
        )
        self.conn.commit()
    
    def get_tenant_stats(self, tenant_id: str):
        # ๐Ÿ“Š Get stats with tenant filtering
        cursor = self.conn.cursor()
        
        users = cursor.execute(
            'SELECT COUNT(*) FROM users WHERE tenant_id = ?',
            (tenant_id,)
        ).fetchone()[0]
        
        tasks = cursor.execute(
            'SELECT COUNT(*) FROM tasks WHERE tenant_id = ?',
            (tenant_id,)
        ).fetchone()[0]
        
        return {'users': users, 'tasks': tasks}

# ๐ŸŽฎ Demo time!
if __name__ == "__main__":
    # Create multi-tenant system
    system = TenantManager(IsolationLevel.ROW_LEVEL)
    
    # Create two companies
    startup = system.create_tenant("startup_inc", "Startup Inc", "free")
    enterprise = system.create_tenant("big_corp", "Big Corporation", "enterprise")
    
    # Add users to each tenant
    startup.add_user("[email protected]", "Alice", "admin")
    startup.add_user("[email protected]", "Bob", "member")
    
    enterprise.add_user("[email protected]", "CEO", "admin")
    enterprise.add_user("[email protected]", "Manager", "admin")
    enterprise.add_user("[email protected]", "Developer", "member")
    
    # Check usage
    print("\n๐Ÿ“ˆ Checking usage limits:")
    startup.get_usage_stats()
    enterprise.get_usage_stats()

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Advanced Pattern: Hybrid Isolation

When youโ€™re ready to level up, try this advanced pattern:

# ๐ŸŽฏ Advanced hybrid isolation strategy
class HybridTenantDB:
    def __init__(self):
        self.shared_db = sqlite3.connect("shared.db")
        self.tenant_dbs = {}  # ๐Ÿ—‚๏ธ Premium tenants get own DB
        self._init_shared_schema()
    
    def _init_shared_schema(self):
        # ๐Ÿ—๏ธ Shared schema for free/basic tenants
        self.shared_db.execute('''
            CREATE TABLE IF NOT EXISTS tenant_data (
                tenant_id TEXT NOT NULL,
                key TEXT NOT NULL,
                value TEXT,
                encrypted BOOLEAN DEFAULT 0,
                PRIMARY KEY (tenant_id, key)
            )
        ''')
    
    def get_connection(self, tenant_id: str, tier: str):
        # ๐Ÿช„ Route based on tenant tier
        if tier in ["enterprise", "dedicated"]:
            # Premium tenants get dedicated database
            if tenant_id not in self.tenant_dbs:
                self.tenant_dbs[tenant_id] = sqlite3.connect(f"{tenant_id}.db")
            return self.tenant_dbs[tenant_id], "dedicated"
        else:
            # Free/Pro tenants share database
            return self.shared_db, "shared"
    
    def migrate_tenant(self, tenant_id: str, from_tier: str, to_tier: str):
        # ๐Ÿš€ Migrate tenant between isolation levels
        print(f"๐Ÿ”„ Migrating {tenant_id} from {from_tier} to {to_tier}")
        
        if from_tier in ["free", "pro"] and to_tier in ["enterprise", "dedicated"]:
            # Move from shared to dedicated
            self._migrate_to_dedicated(tenant_id)
        elif from_tier in ["enterprise", "dedicated"] and to_tier in ["free", "pro"]:
            # Move from dedicated to shared
            self._migrate_to_shared(tenant_id)
        
        print(f"โœ… Migration complete!")

๐Ÿ—๏ธ Advanced Pattern: Sharding by Tenant

For massive scale:

# ๐Ÿš€ Tenant-based sharding for horizontal scaling
class ShardedTenantDB:
    def __init__(self, shard_count: int = 4):
        self.shard_count = shard_count
        self.shards = {}
        self._init_shards()
    
    def _init_shards(self):
        # ๐ŸŽฒ Initialize database shards
        for i in range(self.shard_count):
            self.shards[i] = sqlite3.connect(f"shard_{i}.db")
            self._create_shard_schema(self.shards[i])
    
    def _create_shard_schema(self, conn):
        # ๐Ÿ“‹ Each shard has same schema
        conn.execute('''
            CREATE TABLE IF NOT EXISTS tenant_data (
                tenant_id TEXT NOT NULL,
                data_type TEXT NOT NULL,
                data TEXT,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                PRIMARY KEY (tenant_id, data_type)
            )
        ''')
    
    def _get_shard(self, tenant_id: str):
        # ๐ŸŽฏ Determine shard using consistent hashing
        hash_value = int(hashlib.md5(tenant_id.encode()).hexdigest(), 16)
        shard_id = hash_value % self.shard_count
        return self.shards[shard_id], shard_id
    
    def store_data(self, tenant_id: str, data_type: str, data: str):
        # ๐Ÿ’พ Store data in appropriate shard
        conn, shard_id = self._get_shard(tenant_id)
        conn.execute(
            '''INSERT OR REPLACE INTO tenant_data (tenant_id, data_type, data)
               VALUES (?, ?, ?)''',
            (tenant_id, data_type, data)
        )
        conn.commit()
        print(f"โœจ Stored in shard {shard_id} for tenant {tenant_id}")

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Forgetting Tenant Context

# โŒ Wrong way - no tenant isolation!
def get_all_users():
    return db.execute("SELECT * FROM users").fetchall()  # ๐Ÿ’ฅ Returns ALL tenants' users!

# โœ… Correct way - always filter by tenant!
def get_tenant_users(tenant_id):
    return db.execute(
        "SELECT * FROM users WHERE tenant_id = ?", 
        (tenant_id,)
    ).fetchall()  # ๐Ÿ›ก๏ธ Only returns specific tenant's users

๐Ÿคฏ Pitfall 2: Connection Pool Exhaustion

# โŒ Dangerous - creating connections per tenant!
class BadTenantDB:
    def __init__(self):
        self.connections = {}
    
    def get_connection(self, tenant_id):
        if tenant_id not in self.connections:
            self.connections[tenant_id] = sqlite3.connect(f"{tenant_id}.db")
        return self.connections[tenant_id]  # ๐Ÿ’ฅ Connections never closed!

# โœ… Safe - use connection pooling!
from queue import Queue
import threading

class SafeTenantDB:
    def __init__(self, pool_size=10):
        self.pool = Queue(maxsize=pool_size)
        self._init_pool()
    
    def _init_pool(self):
        # ๐ŸŠ Initialize connection pool
        for _ in range(self.pool.maxsize):
            conn = sqlite3.connect("tenants.db", check_same_thread=False)
            self.pool.put(conn)
    
    @contextmanager
    def get_connection(self):
        # โœ… Safely borrow and return connections
        conn = self.pool.get()
        try:
            yield conn
        finally:
            self.pool.put(conn)

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Always Include Tenant Context: Never query without tenant_id filtering
  2. ๐Ÿ“ Use Database Indexes: Index on tenant_id for performance
  3. ๐Ÿ›ก๏ธ Implement Row-Level Security: Use database features when available
  4. ๐ŸŽจ Plan for Migration: Design to move tenants between isolation levels
  5. โœจ Monitor Resource Usage: Track per-tenant metrics

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Multi-tenant Blog Platform

Create a multi-tenant blogging system:

๐Ÿ“‹ Requirements:

  • โœ… Support multiple blog sites from one application
  • ๐Ÿท๏ธ Each blog has its own posts, authors, and comments
  • ๐Ÿ‘ค Authors can only see/edit their own blogโ€™s content
  • ๐Ÿ“Š Track views and engagement per blog
  • ๐ŸŽจ Each blog needs custom theming!

๐Ÿš€ Bonus Points:

  • Add subscription tiers with different limits
  • Implement data export for individual blogs
  • Create a blog migration feature

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
# ๐ŸŽฏ Multi-tenant blog platform solution!
from datetime import datetime
import json
from typing import Optional, List, Dict

class BlogPlatform:
    def __init__(self):
        self.db = sqlite3.connect("blogs.db")
        self._init_schema()
    
    def _init_schema(self):
        # ๐Ÿ—๏ธ Create multi-tenant blog schema
        self.db.executescript('''
            CREATE TABLE IF NOT EXISTS blogs (
                id TEXT PRIMARY KEY,
                name TEXT NOT NULL,
                domain TEXT UNIQUE,
                theme TEXT DEFAULT '{"color": "blue", "font": "sans-serif"}',
                tier TEXT DEFAULT 'free',
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            );
            
            CREATE TABLE IF NOT EXISTS authors (
                id INTEGER PRIMARY KEY,
                blog_id TEXT NOT NULL,
                email TEXT NOT NULL,
                name TEXT NOT NULL,
                role TEXT DEFAULT 'writer',
                FOREIGN KEY (blog_id) REFERENCES blogs(id),
                UNIQUE(blog_id, email)
            );
            
            CREATE TABLE IF NOT EXISTS posts (
                id INTEGER PRIMARY KEY,
                blog_id TEXT NOT NULL,
                author_id INTEGER NOT NULL,
                title TEXT NOT NULL,
                content TEXT NOT NULL,
                status TEXT DEFAULT 'draft',
                views INTEGER DEFAULT 0,
                published_at TIMESTAMP,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (blog_id) REFERENCES blogs(id),
                FOREIGN KEY (author_id) REFERENCES authors(id)
            );
            
            CREATE TABLE IF NOT EXISTS comments (
                id INTEGER PRIMARY KEY,
                blog_id TEXT NOT NULL,
                post_id INTEGER NOT NULL,
                author_name TEXT NOT NULL,
                content TEXT NOT NULL,
                approved BOOLEAN DEFAULT 0,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (blog_id) REFERENCES blogs(id),
                FOREIGN KEY (post_id) REFERENCES posts(id)
            );
            
            CREATE INDEX idx_posts_blog ON posts(blog_id);
            CREATE INDEX idx_comments_blog ON comments(blog_id);
            CREATE INDEX idx_authors_blog ON authors(blog_id);
        ''')
        self.db.commit()
    
    def create_blog(self, blog_id: str, name: str, domain: str, tier: str = "free"):
        # ๐ŸŽ‰ Create new blog
        theme = {
            "free": {"color": "blue", "font": "sans-serif"},
            "pro": {"color": "custom", "font": "custom", "logo": True},
            "business": {"color": "custom", "font": "custom", "logo": True, "custom_css": True}
        }.get(tier, {})
        
        self.db.execute(
            '''INSERT INTO blogs (id, name, domain, theme, tier) 
               VALUES (?, ?, ?, ?, ?)''',
            (blog_id, name, domain, json.dumps(theme), tier)
        )
        self.db.commit()
        
        print(f"๐ŸŽŠ Blog '{name}' created at {domain}!")
        return Blog(self.db, blog_id, tier)

class Blog:
    def __init__(self, db, blog_id: str, tier: str):
        self.db = db
        self.blog_id = blog_id
        self.tier = tier
        self.limits = {
            "free": {"posts": 10, "authors": 2, "storage_mb": 100},
            "pro": {"posts": 100, "authors": 10, "storage_mb": 1000},
            "business": {"posts": -1, "authors": -1, "storage_mb": 10000}
        }.get(tier, {})
    
    def add_author(self, email: str, name: str, role: str = "writer"):
        # ๐Ÿ‘ค Add author with tier limits
        current_authors = self.db.execute(
            'SELECT COUNT(*) FROM authors WHERE blog_id = ?',
            (self.blog_id,)
        ).fetchone()[0]
        
        if self.limits["authors"] > 0 and current_authors >= self.limits["authors"]:
            print(f"โš ๏ธ Author limit reached! Upgrade to add more.")
            return None
        
        cursor = self.db.execute(
            '''INSERT INTO authors (blog_id, email, name, role) 
               VALUES (?, ?, ?, ?)''',
            (self.blog_id, email, name, role)
        )
        self.db.commit()
        
        print(f"โœ… Added {name} as {role}!")
        return cursor.lastrowid
    
    def create_post(self, author_id: int, title: str, content: str):
        # ๐Ÿ“ Create post with limits
        current_posts = self.db.execute(
            'SELECT COUNT(*) FROM posts WHERE blog_id = ?',
            (self.blog_id,)
        ).fetchone()[0]
        
        if self.limits["posts"] > 0 and current_posts >= self.limits["posts"]:
            print(f"โš ๏ธ Post limit reached! Upgrade to create more.")
            return None
        
        cursor = self.db.execute(
            '''INSERT INTO posts (blog_id, author_id, title, content) 
               VALUES (?, ?, ?, ?)''',
            (self.blog_id, author_id, title, content)
        )
        self.db.commit()
        
        print(f"๐Ÿ“ Created post: {title}")
        return cursor.lastrowid
    
    def publish_post(self, post_id: int):
        # ๐Ÿš€ Publish a post
        self.db.execute(
            '''UPDATE posts SET status = 'published', published_at = CURRENT_TIMESTAMP 
               WHERE id = ? AND blog_id = ?''',
            (post_id, self.blog_id)
        )
        self.db.commit()
        print(f"๐ŸŽ‰ Post {post_id} published!")
    
    def get_analytics(self):
        # ๐Ÿ“Š Get blog analytics
        stats = self.db.execute('''
            SELECT 
                COUNT(DISTINCT p.id) as total_posts,
                COUNT(DISTINCT CASE WHEN p.status = 'published' THEN p.id END) as published_posts,
                SUM(p.views) as total_views,
                COUNT(DISTINCT c.id) as total_comments,
                COUNT(DISTINCT a.id) as total_authors
            FROM blogs b
            LEFT JOIN posts p ON b.id = p.blog_id
            LEFT JOIN comments c ON b.id = c.blog_id
            LEFT JOIN authors a ON b.id = a.blog_id
            WHERE b.id = ?
        ''', (self.blog_id,)).fetchone()
        
        print(f"๐Ÿ“Š Analytics for blog {self.blog_id}:")
        print(f"  ๐Ÿ“ Posts: {stats[1]}/{stats[0]} published")
        print(f"  ๐Ÿ‘€ Total views: {stats[2] or 0}")
        print(f"  ๐Ÿ’ฌ Comments: {stats[3]}")
        print(f"  โœ๏ธ Authors: {stats[4]}")
        
        return dict(zip(['total_posts', 'published_posts', 'total_views', 
                        'total_comments', 'total_authors'], stats))
    
    def export_data(self):
        # ๐Ÿ“ฆ Export blog data
        data = {
            "blog_id": self.blog_id,
            "posts": [],
            "authors": [],
            "comments": []
        }
        
        # Export posts
        posts = self.db.execute(
            'SELECT * FROM posts WHERE blog_id = ?',
            (self.blog_id,)
        ).fetchall()
        data["posts"] = [dict(zip([d[0] for d in self.db.description], post)) 
                        for post in posts]
        
        print(f"๐Ÿ“ฆ Exported {len(data['posts'])} posts")
        return data

# ๐ŸŽฎ Test it out!
platform = BlogPlatform()

# Create two blogs
tech_blog = platform.create_blog("tech_tales", "Tech Tales", "techtales.com", "pro")
food_blog = platform.create_blog("foodie", "Foodie Finds", "foodiefinds.com", "free")

# Add authors
john_id = tech_blog.add_author("[email protected]", "John", "admin")
jane_id = tech_blog.add_author("[email protected]", "Jane", "writer")

# Create posts
post1 = tech_blog.create_post(john_id, "Python Multi-tenancy Guide", "Content here...")
post2 = tech_blog.create_post(jane_id, "Database Patterns", "More content...")

# Publish and analyze
tech_blog.publish_post(post1)
tech_blog.get_analytics()

# Check limits on free tier
foodie_id = food_blog.add_author("[email protected]", "Chef", "admin")
for i in range(3):
    food_blog.add_author(f"writer{i}@foodie.com", f"Writer {i}", "writer")

๐ŸŽ“ Key Takeaways

Youโ€™ve learned so much! Hereโ€™s what you can now do:

  • โœ… Design multi-tenant architectures with confidence ๐Ÿ’ช
  • โœ… Choose the right isolation level for your needs ๐Ÿ›ก๏ธ
  • โœ… Implement secure data separation in real projects ๐ŸŽฏ
  • โœ… Handle tenant migrations like a pro ๐Ÿ›
  • โœ… Build scalable SaaS applications with Python! ๐Ÿš€

Remember: Multi-tenancy is about balance - security, performance, and cost. Choose wisely! ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered multi-tenancy database patterns!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the blog platform exercise above
  2. ๐Ÿ—๏ธ Build a small SaaS project using these patterns
  3. ๐Ÿ“š Explore database-specific features (PostgreSQL schemas, MySQL databases)
  4. ๐ŸŒŸ Share your multi-tenant architecture experiences!

Remember: Every successful SaaS started with understanding multi-tenancy. Keep building, keep learning, and most importantly, have fun! ๐Ÿš€


Happy coding! ๐ŸŽ‰๐Ÿš€โœจ