Weekend Project: Creating a Data-Driven Application Using Multiple Database Technologies

Applying George Polya's 4-Step Problem Solving Approach

Introduction to the Weekend Project

In this weekend project, you will build a data-driven application that leverages multiple database technologies. This experience will solidify your understanding of various database paradigms and help you make informed decisions about when to use each technology in real-world scenarios.

Throughout the project, we'll use George Polya's famous 4-step problem-solving framework to guide our approach:

graph TD A[1. Understand the Problem] --> B[2. Devise a Plan] B --> C[3. Execute the Plan] C --> D[4. Review and Reflect] D -.-> A style A fill:#f9d71c,stroke:#333,stroke-width:2px style B fill:#a1d9ff,stroke:#333,stroke-width:2px style C fill:#a1ffa8,stroke:#333,stroke-width:2px style D fill:#ffa1a1,stroke:#333,stroke-width:2px

This approach, developed by mathematician George Polya in his book "How to Solve It" (1945), provides a systematic framework for tackling complex problems. It's particularly valuable in software development where requirements and constraints must be carefully analyzed before building solutions.

Project Overview: Multi-Database Task Management System

You will create a task management system called TaskForge that uses different database technologies for different aspects of the application:

graph TD A[TaskForge Application] --> B[MongoDB] A --> C[MySQL/PostgreSQL] A --> D[Redis] B --> E[Task Documents] B --> F[User Profiles] B --> G[Comments/Attachments] C --> H[Team Structures] C --> I[User Permissions] C --> J[Audit Logs] D --> K[Caching] D --> L[Session Management] D --> M[Notifications Queue] style A fill:#f96,stroke:#333,stroke-width:2px style B fill:#4DB33D,stroke:#333,stroke-width:2px style C fill:#3D85B3,stroke:#333,stroke-width:2px style D fill:#DC382D,stroke:#333,stroke-width:2px

Step 1: Understand the Problem

The first step in Polya's method is to clearly understand the problem. For our TaskForge application, we need to identify the requirements, constraints, and use cases.

Requirements Analysis

Database Technology Selection Rationale

Why multiple databases?

Many modern applications use a polyglot persistence approach—using different database technologies for different aspects of the application based on their strengths. This is not just a theoretical exercise; companies like Netflix, Uber, and Airbnb employ multiple database technologies in their production systems.

Database Strengths Use Cases in TaskForge
MongoDB
  • Flexible schema for evolving data models
  • JSON-like documents match application objects
  • Good for heterogeneous, nested, and unstructured data
  • Horizontal scaling for high volume data
  • Task details (which may have varying fields depending on task type)
  • User profiles (which may contain varying attributes)
  • Comments and attachments (unstructured content)
MySQL/PostgreSQL
  • Strong consistency and ACID transactions
  • Complex joins and relationships
  • Mature query optimization
  • Well-suited for reporting and analytics
  • Team structures and hierarchies
  • User roles and permissions (requiring consistency)
  • Audit logs (requiring guaranteed writes and query capabilities)
  • Reporting data
Redis
  • Extremely fast in-memory operations
  • Built-in data structures (lists, sets, sorted sets)
  • Pub/sub messaging capabilities
  • Automatic expiration of data
  • Session management
  • Caching frequently accessed data
  • Real-time notifications
  • Rate limiting
  • Temporary data storage

Understanding the Challenges

Analogy: The Master Chef's Kitchen

Think of our multi-database approach like a master chef's kitchen. Different tools and cooking methods are used for different aspects of a meal:

  • MongoDB is like the versatile food processor - it can handle a wide variety of ingredients and preparations, adapting to whatever you need to create.
  • MySQL/PostgreSQL is like the precision cooking equipment - when exact measurements, consistent results, and traditional techniques are required.
  • Redis is like the quick-access pantry and mise en place - keeping frequently used ingredients ready and organized for immediate use.

A master chef knows when to use each tool based on the dish being prepared, just as we'll use each database based on the specific data needs.

Step 2: Devise a Plan

In this step, we'll outline the architectural design of our application and plan how we'll integrate the different database technologies.

System Architecture

flowchart TB Client[Client Applications] <--> API[API Layer] subgraph Backend API <--> Services[Service Layer] Services <--> DM[Data Management Layer] end DM <--> MongoDB[(MongoDB)] DM <--> MySQL[(MySQL/PostgreSQL)] DM <--> Redis[(Redis)] Services --> Auth[Authentication Service] Services --> Task[Task Service] Services --> Team[Team Service] Services --> Notif[Notification Service] Services --> Analytics[Analytics Service] style Client fill:#f9f9f9,stroke:#333,stroke-width:1px style Backend fill:#ebf5ff,stroke:#333,stroke-width:1px

Data Models

Let's define the core data models for each database:

MongoDB Models


// User Model
const userSchema = new mongoose.Schema({
    userId: String,  // UUID, links to relational DB
    email: {
        type: String,
        required: true,
        unique: true
    },
    passwordHash: String,
    firstName: String,
    lastName: String,
    profilePicture: String,
    bio: String,
    preferences: {
        theme: {
            type: String,
            enum: ['light', 'dark', 'system'],
            default: 'system'
        },
        notifications: {
            email: {
                type: Boolean,
                default: true
            },
            push: {
                type: Boolean,
                default: true
            },
            frequency: {
                type: String,
                enum: ['immediate', 'hourly', 'daily', 'weekly'],
                default: 'immediate'
            }
        }
    },
    metadata: {
        created: {
            type: Date,
            default: Date.now
        },
        lastLogin: Date,
        lastPasswordChange: Date
    }
});

// Task Model
const taskSchema = new mongoose.Schema({
    taskId: String,  // UUID, links to relational DB if needed
    title: {
        type: String,
        required: true
    },
    description: String,
    status: {
        type: String,
        enum: ['backlog', 'todo', 'in_progress', 'in_review', 'done'],
        default: 'todo'
    },
    priority: {
        type: String,
        enum: ['low', 'medium', 'high', 'urgent'],
        default: 'medium'
    },
    assigneeId: String,
    creatorId: String,
    teamId: String,
    dueDate: Date,
    tags: [String],
    attachments: [{
        name: String,
        url: String,
        size: Number,
        mimetype: String,
        uploadedAt: {
            type: Date,
            default: Date.now
        },
        uploadedBy: String
    }],
    customFields: mongoose.Schema.Types.Mixed,  // Flexible fields depending on task type
    metadata: {
        created: {
            type: Date,
            default: Date.now
        },
        updated: Date
    }
});

// Comment Model
const commentSchema = new mongoose.Schema({
    taskId: {
        type: String,
        required: true
    },
    authorId: {
        type: String,
        required: true
    },
    content: {
        type: String,
        required: true
    },
    attachments: [{
        name: String,
        url: String,
        size: Number,
        mimetype: String
    }],
    mentionedUsers: [String],
    created: {
        type: Date,
        default: Date.now
    },
    edited: Boolean,
    lastEditedAt: Date
});
            

MySQL/PostgreSQL Models


-- Users Table
CREATE TABLE users (
    user_id VARCHAR(36) PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- Teams Table
CREATE TABLE teams (
    team_id VARCHAR(36) PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    created_by VARCHAR(36) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (created_by) REFERENCES users(user_id)
);

-- Team Memberships Table
CREATE TABLE team_memberships (
    team_id VARCHAR(36) NOT NULL,
    user_id VARCHAR(36) NOT NULL,
    role ENUM('admin', 'member', 'guest') NOT NULL DEFAULT 'member',
    joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (team_id, user_id),
    FOREIGN KEY (team_id) REFERENCES teams(team_id),
    FOREIGN KEY (user_id) REFERENCES users(user_id)
);

-- Tasks Table (minimal reference info - detail in MongoDB)
CREATE TABLE tasks (
    task_id VARCHAR(36) PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    team_id VARCHAR(36) NOT NULL,
    assignee_id VARCHAR(36),
    status ENUM('backlog', 'todo', 'in_progress', 'in_review', 'done') DEFAULT 'todo',
    created_by VARCHAR(36) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (team_id) REFERENCES teams(team_id),
    FOREIGN KEY (assignee_id) REFERENCES users(user_id),
    FOREIGN KEY (created_by) REFERENCES users(user_id)
);

-- Audit Logs Table
CREATE TABLE audit_logs (
    log_id INT AUTO_INCREMENT PRIMARY KEY,
    entity_type ENUM('user', 'team', 'task', 'comment') NOT NULL,
    entity_id VARCHAR(36) NOT NULL,
    action ENUM('create', 'update', 'delete', 'assign', 'status_change') NOT NULL,
    actor_id VARCHAR(36) NOT NULL,
    action_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    previous_state JSON,
    new_state JSON,
    ip_address VARCHAR(45),
    user_agent TEXT,
    FOREIGN KEY (actor_id) REFERENCES users(user_id)
);

-- Permissions Table
CREATE TABLE permissions (
    permission_id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    description TEXT
);

-- Role Permissions Table
CREATE TABLE role_permissions (
    team_id VARCHAR(36) NOT NULL,
    role ENUM('admin', 'member', 'guest') NOT NULL,
    permission_id INT NOT NULL,
    PRIMARY KEY (team_id, role, permission_id),
    FOREIGN KEY (team_id) REFERENCES teams(team_id),
    FOREIGN KEY (permission_id) REFERENCES permissions(permission_id)
);
            

Redis Data Structures


// Session Management
// Key: `session:{sessionId}`
// Value: JSON string with session data
{
    "userId": "user123",
    "email": "user@example.com",
    "name": "John Doe",
    "issuedAt": 1620000000,
    "expiresAt": 1620086400
}

// User Sessions Index
// Key: `user:sessions:{userId}`
// Value: Set of active sessionIds
["session1", "session2", "session3"]

// Cache for User Profile
// Key: `user:profile:{userId}`
// Value: JSON string with user profile
// TTL: 3600 seconds (1 hour)
{
    "userId": "user123",
    "name": "John Doe",
    "email": "user@example.com",
    "profilePicture": "https://example.com/pic.jpg"
}

// Cache for Task Details
// Key: `task:{taskId}`
// Value: JSON string with task details
// TTL: 300 seconds (5 minutes)
{
    "taskId": "task123",
    "title": "Implement Redis",
    "status": "in_progress",
    "assignee": "user123",
    "dueDate": "2023-08-01"
}

// Notification Queue
// Key: `notifications:queue`
// Value: List of notification payloads
[
    "{\"userId\":\"user123\",\"message\":\"New task assigned\",\"taskId\":\"task123\"}",
    "{\"userId\":\"user456\",\"message\":\"Task completed\",\"taskId\":\"task789\"}"
]

// User Notification Counter
// Key: `user:notifications:count:{userId}`
// Value: Integer counting unread notifications
5

// Online User Tracking
// Key: `online:users`
// Value: Sorted Set with userId and last activity timestamp
["user123", 1620000000, "user456", 1620000030]

// Rate Limiting
// Key: `ratelimit:api:{userId}`
// Value: Integer count of API calls in time window
// TTL: 60 seconds
15
            

Integration Strategy

We need a clear strategy for how the databases will interact:

sequenceDiagram participant Client participant API participant MySQL participant MongoDB participant Redis Client->>API: Create new task API->>MySQL: Begin transaction API->>MySQL: Insert basic task info API->>MongoDB: Store detailed task document API->>MySQL: Commit transaction if MongoDB success API->>MySQL: Log audit record API->>Redis: Cache task details API->>Redis: Add notification to queue API->>Client: Return success response

Key integration patterns:

Project Planning

Breaking down the implementation into manageable tasks:

  1. Day 1 (Morning): Setup and Configuration
    • Set up project structure and initialize Git repository
    • Install necessary packages and dependencies
    • Configure MongoDB, MySQL/PostgreSQL, and Redis connections
    • Create basic Express server and API structure
  2. Day 1 (Afternoon): Core Models and Database Schema
    • Implement MongoDB schemas
    • Create SQL database tables
    • Define Redis data structures
    • Implement database connection management
  3. Day 2 (Morning): User Management and Authentication
    • Implement user registration and authentication
    • Set up session management with Redis
    • Create profile management functionality
  4. Day 2 (Afternoon): Task and Team Management
    • Implement task CRUD operations
    • Create team management functionality
    • Set up permissions and access control
  5. Day 3 (Morning): Comments, Notifications, and Audit Logging
    • Add comment functionality
    • Implement notification system using Redis
    • Create audit logging for all operations
  6. Day 3 (Afternoon): Frontend Integration and Testing
    • Create a simple frontend for demonstration
    • Test all functionality end-to-end
    • Fix bugs and refine implementation

Step 3: Execute the Plan

Now we'll implement our TaskForge application according to our planned architecture. Let's work through the key implementation aspects.

Project Setup


mkdir taskforge
cd taskforge
npm init -y
npm install express mongoose mysql2 redis dotenv cors helmet jsonwebtoken bcrypt morgan
npm install --save-dev nodemon

# Create project structure
mkdir -p src/{config,controllers,models,routes,services,middleware,utils}
touch .env .gitignore README.md
            

Database Configuration


// src/config/mongodb.js
const mongoose = require('mongoose');

const connectMongoDB = async () => {
    try {
        await mongoose.connect(process.env.MONGODB_URI, {
            useNewUrlParser: true,
            useUnifiedTopology: true
        });
        console.log('MongoDB connected successfully');
    } catch (error) {
        console.error('MongoDB connection error:', error);
        process.exit(1);
    }
};

module.exports = connectMongoDB;

// src/config/mysql.js
const mysql = require('mysql2/promise');

let pool;

const connectMySQL = async () => {
    try {
        pool = mysql.createPool({
            host: process.env.MYSQL_HOST,
            user: process.env.MYSQL_USER,
            password: process.env.MYSQL_PASSWORD,
            database: process.env.MYSQL_DATABASE,
            waitForConnections: true,
            connectionLimit: 10,
            queueLimit: 0
        });
        
        // Test connection
        const connection = await pool.getConnection();
        console.log('MySQL connected successfully');
        connection.release();
    } catch (error) {
        console.error('MySQL connection error:', error);
        process.exit(1);
    }
};

const getPool = () => pool;

module.exports = {
    connectMySQL,
    getPool
};

// src/config/redis.js
const redis = require('redis');
const { promisify } = require('util');

let client;
let getAsync;
let setAsync;
let delAsync;
let expireAsync;

const connectRedis = async () => {
    try {
        client = redis.createClient({
            host: process.env.REDIS_HOST,
            port: process.env.REDIS_PORT,
            password: process.env.REDIS_PASSWORD
        });
        
        // Promisify Redis commands
        getAsync = promisify(client.get).bind(client);
        setAsync = promisify(client.set).bind(client);
        delAsync = promisify(client.del).bind(client);
        expireAsync = promisify(client.expire).bind(client);
        
        client.on('connect', () => {
            console.log('Redis connected successfully');
        });
        
        client.on('error', (err) => {
            console.error('Redis connection error:', err);
        });
    } catch (error) {
        console.error('Redis initialization error:', error);
        process.exit(1);
    }
};

module.exports = {
    connectRedis,
    getClient: () => client,
    getAsync,
    setAsync,
    delAsync,
    expireAsync
};
            

Key Implementation Components

Service Layer for Task Management


// src/services/taskService.js
const Task = require('../models/mongodb/task');
const { getPool } = require('../config/mysql');
const { setAsync, delAsync } = require('../config/redis');
const { v4: uuidv4 } = require('uuid');

class TaskService {
    async createTask(taskData, userId) {
        const mysqlPool = getPool();
        const connection = await mysqlPool.getConnection();
        
        try {
            // Begin transaction
            await connection.beginTransaction();
            
            // Generate a UUID for the task
            const taskId = uuidv4();
            
            // Insert basic task info into MySQL
            const sqlQuery = `
                INSERT INTO tasks (task_id, title, team_id, assignee_id, created_by)
                VALUES (?, ?, ?, ?, ?)
            `;
            
            await connection.execute(sqlQuery, [
                taskId,
                taskData.title,
                taskData.teamId,
                taskData.assigneeId || null,
                userId
            ]);
            
            // Create detailed task document in MongoDB
            const task = new Task({
                taskId,
                title: taskData.title,
                description: taskData.description,
                status: taskData.status || 'todo',
                priority: taskData.priority || 'medium',
                assigneeId: taskData.assigneeId,
                creatorId: userId,
                teamId: taskData.teamId,
                dueDate: taskData.dueDate,
                tags: taskData.tags || [],
                customFields: taskData.customFields || {}
            });
            
            await task.save();
            
            // Log audit record
            const auditQuery = `
                INSERT INTO audit_logs 
                (entity_type, entity_id, action, actor_id, new_state)
                VALUES ('task', ?, 'create', ?, ?)
            `;
            
            await connection.execute(auditQuery, [
                taskId,
                userId,
                JSON.stringify({ 
                    title: taskData.title,
                    teamId: taskData.teamId,
                    assigneeId: taskData.assigneeId
                })
            ]);
            
            // Commit transaction
            await connection.commit();
            
            // Cache task in Redis (5 minutes TTL)
            await setAsync(`task:${taskId}`, JSON.stringify({
                taskId,
                title: taskData.title,
                status: taskData.status || 'todo',
                assigneeId: taskData.assigneeId,
                teamId: taskData.teamId
            }), 'EX', 300);
            
            // If task is assigned, add to notification queue
            if (taskData.assigneeId) {
                const notification = {
                    userId: taskData.assigneeId,
                    type: 'task_assigned',
                    data: {
                        taskId,
                        taskTitle: taskData.title
                    },
                    timestamp: new Date().toISOString()
                };
                
                await this.queueNotification(notification);
            }
            
            return task;
        } catch (error) {
            // Rollback transaction on error
            await connection.rollback();
            throw error;
        } finally {
            connection.release();
        }
    }
    
    async getTaskById(taskId) {
        try {
            // Try to get from cache first
            const cachedTask = await getAsync(`task:${taskId}`);
            if (cachedTask) {
                return JSON.parse(cachedTask);
            }
            
            // If not in cache, get from MongoDB
            const task = await Task.findOne({ taskId });
            if (!task) {
                throw new Error('Task not found');
            }
            
            // Update cache
            await setAsync(`task:${taskId}`, JSON.stringify(task), 'EX', 300);
            
            return task;
        } catch (error) {
            throw error;
        }
    }
    
    async updateTask(taskId, updateData, userId) {
        const mysqlPool = getPool();
        const connection = await mysqlPool.getConnection();
        
        try {
            await connection.beginTransaction();
            
            // Get current task state for audit logging
            const currentTask = await Task.findOne({ taskId });
            if (!currentTask) {
                throw new Error('Task not found');
            }
            
            // Update in MongoDB
            const updatedTask = await Task.findOneAndUpdate(
                { taskId },
                { $set: updateData },
                { new: true }
            );
            
            // Update in MySQL if necessary fields changed
            if (updateData.title || updateData.assigneeId || updateData.status) {
                const updates = [];
                const params = [];
                
                if (updateData.title) {
                    updates.push('title = ?');
                    params.push(updateData.title);
                }
                
                if (updateData.assigneeId !== undefined) {
                    updates.push('assignee_id = ?');
                    params.push(updateData.assigneeId);
                }
                
                if (updateData.status) {
                    updates.push('status = ?');
                    params.push(updateData.status);
                }
                
                if (updates.length > 0) {
                    params.push(taskId);
                    
                    const sqlQuery = `
                        UPDATE tasks
                        SET ${updates.join(', ')}, updated_at = CURRENT_TIMESTAMP
                        WHERE task_id = ?
                    `;
                    
                    await connection.execute(sqlQuery, params);
                }
            }
            
            // Log audit record
            const auditQuery = `
                INSERT INTO audit_logs 
                (entity_type, entity_id, action, actor_id, previous_state, new_state)
                VALUES ('task', ?, 'update', ?, ?, ?)
            `;
            
            await connection.execute(auditQuery, [
                taskId,
                userId,
                JSON.stringify(currentTask),
                JSON.stringify(updatedTask)
            ]);
            
            await connection.commit();
            
            // Invalidate cache
            await delAsync(`task:${taskId}`);
            
            // If assignee changed, send notification
            if (updateData.assigneeId && updateData.assigneeId !== currentTask.assigneeId) {
                const notification = {
                    userId: updateData.assigneeId,
                    type: 'task_assigned',
                    data: {
                        taskId,
                        taskTitle: updateData.title || currentTask.title
                    },
                    timestamp: new Date().toISOString()
                };
                
                await this.queueNotification(notification);
            }
            
            // If status changed, notify creator
            if (updateData.status && updateData.status !== currentTask.status) {
                const notification = {
                    userId: currentTask.creatorId,
                    type: 'task_status_changed',
                    data: {
                        taskId,
                        taskTitle: updateData.title || currentTask.title,
                        previousStatus: currentTask.status,
                        newStatus: updateData.status
                    },
                    timestamp: new Date().toISOString()
                };
                
                await this.queueNotification(notification);
            }
            
            return updatedTask;
        } catch (error) {
            await connection.rollback();
            throw error;
        } finally {
            connection.release();
        }
    }
    
    async deleteTask(taskId, userId) {
        const mysqlPool = getPool();
        const connection = await mysqlPool.getConnection();
        
        try {
            await connection.beginTransaction();
            
            // Get current task for audit
            const task = await Task.findOne({ taskId });
            if (!task) {
                throw new Error('Task not found');
            }
            
            // Delete from MongoDB
            await Task.deleteOne({ taskId });
            
            // Delete from MySQL
            await connection.execute('DELETE FROM tasks WHERE task_id = ?', [taskId]);
            
            // Log audit record
            const auditQuery = `
                INSERT INTO audit_logs 
                (entity_type, entity_id, action, actor_id, previous_state)
                VALUES ('task', ?, 'delete', ?, ?)
            `;
            
            await connection.execute(auditQuery, [
                taskId,
                userId,
                JSON.stringify(task)
            ]);
            
            await connection.commit();
            
            // Invalidate cache
            await delAsync(`task:${taskId}`);
            
            return { success: true, message: 'Task deleted successfully' };
        } catch (error) {
            await connection.rollback();
            throw error;
        } finally {
            connection.release();
        }
    }
    
    async queueNotification(notification) {
        const client = require('../config/redis').getClient();
        client.lpush('notifications:queue', JSON.stringify(notification));
        client.incr(`user:notifications:count:${notification.userId}`);
    }
}

module.exports = new TaskService();
            

Authentication with Redis Session Management


// src/services/authService.js
const { getPool } = require('../config/mysql');
const User = require('../models/mongodb/user');
const { v4: uuidv4 } = require('uuid');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const redisConfig = require('../config/redis');

class AuthService {
    async register(userData) {
        const mysqlPool = getPool();
        const connection = await mysqlPool.getConnection();
        
        try {
            // Check if email already exists
            const [existingUsers] = await connection.execute(
                'SELECT * FROM users WHERE email = ?',
                [userData.email]
            );
            
            if (existingUsers.length > 0) {
                throw new Error('Email already registered');
            }
            
            const userId = uuidv4();
            const saltRounds = 10;
            const passwordHash = await bcrypt.hash(userData.password, saltRounds);
            
            // Begin transaction
            await connection.beginTransaction();
            
            // Create user in MySQL
            await connection.execute(
                'INSERT INTO users (user_id, email, password_hash) VALUES (?, ?, ?)',
                [userId, userData.email, passwordHash]
            );
            
            // Create user profile in MongoDB
            const userProfile = new User({
                userId,
                email: userData.email,
                firstName: userData.firstName,
                lastName: userData.lastName,
                passwordHash // For demo only - in production, consider not storing this in MongoDB
            });
            
            await userProfile.save();
            
            // Commit transaction
            await connection.commit();
            
            return {
                userId,
                email: userData.email,
                firstName: userData.firstName,
                lastName: userData.lastName
            };
        } catch (error) {
            await connection.rollback();
            throw error;
        } finally {
            connection.release();
        }
    }
    
    async login(email, password) {
        const mysqlPool = getPool();
        
        try {
            // Get user from MySQL
            const [users] = await mysqlPool.execute(
                'SELECT * FROM users WHERE email = ?',
                [email]
            );
            
            if (users.length === 0) {
                throw new Error('Invalid credentials');
            }
            
            const user = users[0];
            
            // Verify password
            const passwordMatch = await bcrypt.compare(password, user.password_hash);
            if (!passwordMatch) {
                throw new Error('Invalid credentials');
            }
            
            // Get user profile from MongoDB
            const userProfile = await User.findOne({ userId: user.user_id });
            
            // Create JWT
            const token = jwt.sign(
                { userId: user.user_id, email: user.email },
                process.env.JWT_SECRET,
                { expiresIn: '1h' }
            );
            
            // Create session in Redis
            const sessionId = uuidv4();
            const sessionData = {
                userId: user.user_id,
                email: user.email,
                name: userProfile ? `${userProfile.firstName} ${userProfile.lastName}` : '',
                issuedAt: Math.floor(Date.now() / 1000),
                expiresAt: Math.floor(Date.now() / 1000) + 3600 // 1 hour
            };
            
            // Store session
            await redisConfig.setAsync(`session:${sessionId}`, JSON.stringify(sessionData), 'EX', 3600);
            
            // Add to user sessions list
            const client = redisConfig.getClient();
            client.sadd(`user:sessions:${user.user_id}`, sessionId);
            
            // Update last login
            await mysqlPool.execute(
                'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE user_id = ?',
                [user.user_id]
            );
            
            if (userProfile) {
                userProfile.metadata.lastLogin = new Date();
                await userProfile.save();
                
                // Cache user profile
                await redisConfig.setAsync(
                    `user:profile:${user.user_id}`,
                    JSON.stringify({
                        userId: user.user_id,
                        name: `${userProfile.firstName} ${userProfile.lastName}`,
                        email: user.email,
                        profilePicture: userProfile.profilePicture
                    }),
                    'EX',
                    3600
                );
            }
            
            return {
                token,
                sessionId,
                user: {
                    userId: user.user_id,
                    email: user.email,
                    firstName: userProfile?.firstName,
                    lastName: userProfile?.lastName
                }
            };
        } catch (error) {
            throw error;
        }
    }
    
    async logout(sessionId, userId) {
        try {
            // Delete session
            await redisConfig.delAsync(`session:${sessionId}`);
            
            // Remove from user sessions
            const client = redisConfig.getClient();
            client.srem(`user:sessions:${userId}`, sessionId);
            
            return { success: true };
        } catch (error) {
            throw error;
        }
    }
    
    async verifySession(sessionId) {
        try {
            const sessionData = await redisConfig.getAsync(`session:${sessionId}`);
            if (!sessionData) {
                return null;
            }
            
            const session = JSON.parse(sessionData);
            const now = Math.floor(Date.now() / 1000);
            
            if (session.expiresAt < now) {
                await redisConfig.delAsync(`session:${sessionId}`);
                return null;
            }
            
            return session;
        } catch (error) {
            throw error;
        }
    }
}

module.exports = new AuthService();
            

API Routes and Controllers


// src/controllers/taskController.js
const taskService = require('../services/taskService');

exports.createTask = async (req, res) => {
    try {
        const task = await taskService.createTask(req.body, req.user.userId);
        res.status(201).json(task);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
};

exports.getTask = async (req, res) => {
    try {
        const task = await taskService.getTaskById(req.params.id);
        res.status(200).json(task);
    } catch (error) {
        res.status(404).json({ error: error.message });
    }
};

exports.updateTask = async (req, res) => {
    try {
        const task = await taskService.updateTask(
            req.params.id,
            req.body,
            req.user.userId
        );
        res.status(200).json(task);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
};

exports.deleteTask = async (req, res) => {
    try {
        const result = await taskService.deleteTask(req.params.id, req.user.userId);
        res.status(200).json(result);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
};

// src/routes/taskRoutes.js
const express = require('express');
const router = express.Router();
const taskController = require('../controllers/taskController');
const auth = require('../middleware/auth');

router.post('/', auth, taskController.createTask);
router.get('/:id', auth, taskController.getTask);
router.put('/:id', auth, taskController.updateTask);
router.delete('/:id', auth, taskController.deleteTask);

module.exports = router;

// src/middleware/auth.js
const authService = require('../services/authService');

module.exports = async (req, res, next) => {
    try {
        const sessionId = req.headers['x-session-id'];
        if (!sessionId) {
            return res.status(401).json({ error: 'Authentication required' });
        }
        
        const session = await authService.verifySession(sessionId);
        if (!session) {
            return res.status(401).json({ error: 'Invalid or expired session' });
        }
        
        req.user = {
            userId: session.userId,
            email: session.email
        };
        
        next();
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
};
            

Server Setup


// src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
require('dotenv').config();

// Database connections
const connectMongoDB = require('./config/mongodb');
const { connectMySQL } = require('./config/mysql');
const { connectRedis } = require('./config/redis');

// Routes
const authRoutes = require('./routes/authRoutes');
const taskRoutes = require('./routes/taskRoutes');
const teamRoutes = require('./routes/teamRoutes');
const userRoutes = require('./routes/userRoutes');

// Initialize Express app
const app = express();

// Middleware
app.use(cors());
app.use(helmet());
app.use(morgan('dev'));
app.use(express.json());

// Routes
app.use('/api/auth', authRoutes);
app.use('/api/tasks', taskRoutes);
app.use('/api/teams', teamRoutes);
app.use('/api/users', userRoutes);

// Connect to databases
const startServer = async () => {
    try {
        await connectMongoDB();
        await connectMySQL();
        await connectRedis();
        
        const PORT = process.env.PORT || 3000;
        app.listen(PORT, () => {
            console.log(`Server running on port ${PORT}`);
        });
    } catch (error) {
        console.error('Failed to start server:', error);
        process.exit(1);
    }
};

startServer();
            

Implementation Challenges and Solutions

Challenge 1: Data Consistency Across Databases

Problem: Ensuring data remains consistent when operations span multiple databases.

Solution: Implement a distributed transaction pattern using a combination of:

  • Two-phase operations where we attempt each database operation in sequence
  • Rollback mechanisms for error handling
  • Event-driven architecture for propagating changes
  • Idempotent operations that can be safely retried

Code example: See the createTask and updateTask methods in the TaskService.

Challenge 2: Efficient Caching Strategy

Problem: Determining what to cache, for how long, and how to handle cache invalidation.

Solution: Implement a strategic caching approach:

  • Cache frequently accessed, relatively stable data (user profiles, task details)
  • Use appropriate TTLs based on data change frequency
  • Implement explicit cache invalidation on writes
  • Use cache-aside pattern (check cache first, fall back to database)

Code example: See the getTaskById method in the TaskService.

Challenge 3: Session Management

Problem: Creating a secure, scalable session management system.

Solution: Use Redis as a session store:

  • Store session data with expiration times
  • Track user sessions for account management
  • Enable fast session validation
  • Support session revocation

Code example: See the AuthService's login and verifySession methods.

Step 4: Review and Reflect

The final step in Polya's problem-solving approach is to review our solution and reflect on what we've learned. This helps us improve our understanding and identify strategies for future problems.

Reviewing Our Solution

Architecture Analysis

Lessons Learned

mindmap root((Multi-Database
Lessons)) Database Selection Choose technologies based on data characteristics Consider query patterns and access frequency Balance between consistency and performance Data Management Define clear source of truth for each entity Implement strategic data duplication Use caching to reduce database load Develop consistent data access patterns Integration Patterns Adopt event-driven architecture Implement bounded transactions Design for eventual consistency Create resilient failure handling Development Practices Abstract database access through services Implement comprehensive logging Develop thorough testing strategies Create environment-specific configurations

When to Use Each Database Technology

Through building our TaskForge application, we've gained practical insights about when to use different database technologies:

Database Type Best Used For Avoid Using For
MongoDB
  • Documents with varying structures
  • Rapidly evolving schemas
  • Nested/hierarchical data
  • High write throughput scenarios
  • When schema flexibility is more important than complex joins
  • Complex transactions across multiple collections
  • Highly normalized relational data
  • When complex joins are frequent
  • When ACID guarantees are critical
MySQL/PostgreSQL
  • Structured data with clear relationships
  • When data integrity is critical
  • Complex queries with multiple joins
  • When transactions must be ACID compliant
  • For reporting and analytics
  • Extremely high write throughput
  • Highly variable document structures
  • When horizontal scaling is a primary concern
  • When schema flexibility is more important than structure
Redis
  • Caching frequently accessed data
  • Session management
  • Real-time leaderboards and counters
  • Pub/sub messaging
  • Rate limiting and temporary data
  • Primary persistent data storage
  • Complex querying needs
  • Very large datasets that exceed memory
  • When data loss cannot be tolerated

Real-World Applications of Multi-Database Architecture

Our TaskForge project mirrors approaches used by major technology companies:

Analogy: The Orchestra

A multi-database application is like an orchestra. Each instrument (database) has its own unique sound and capabilities:

  • MongoDB is like the string section - versatile, expressive, and adaptable to different musical styles
  • MySQL/PostgreSQL is like the brass section - powerful, structured, and provides a strong foundation
  • Redis is like the percussion section - keeps the tempo, provides immediate impact, and ties everything together

A skilled conductor (application architecture) knows how to leverage each section's strengths while ensuring they all work together harmoniously. Sometimes a solo is appropriate (using a single database), but complex pieces require the full orchestra working in concert.

Practical Exercises

Complete the following exercises to further enhance your understanding of multi-database applications:

  1. Database Integration Exercise

    Extend the TaskForge application to include one additional feature that leverages all three databases. Options include:

    • A reporting/analytics system that aggregates data across databases
    • A comprehensive search feature that searches across all data types
    • A real-time collaboration system using Redis pub/sub
  2. Performance Optimization

    Identify potential performance bottlenecks in the application and implement optimizations:

    • Add appropriate indexes to MongoDB and MySQL
    • Optimize the caching strategy
    • Implement query optimization techniques
    • Add performance logging and monitoring
  3. Error Resilience

    Improve the application's resilience to database failures:

    • Implement connection pooling and retry mechanisms
    • Add circuit breakers for database operations
    • Create a fallback strategy when a database is unavailable
    • Implement proper logging and alerting for database issues
  4. Alternative Implementation

    Re-implement one aspect of the application using a different approach:

    • Replace direct Redis operations with a message queue system
    • Try using MongoDB transactions instead of the two-phase approach
    • Implement a CQRS (Command Query Responsibility Segregation) pattern
  5. Documentation and Reflection

    Create comprehensive documentation for your TaskForge application:

    • Document the architecture and database design decisions
    • Create API documentation
    • Write a reflection on challenges faced and solutions implemented
    • Suggest future improvements

Further Resources