Skip to content
advanced Phase 20 · Full Stack Capstone Project

Capstone Project Planning

Design a production-ready architecture with authentication, real-time features, and scalable database design.

1h
0 problems
Topic Progress 0%

Production Architecture Design

Production Architecture Design

A well-designed architecture is the foundation of any successful full stack application. Before writing code, you must plan how components communicate, where data flows, and how the system scales under load.

Layered Architecture Pattern

Most production applications follow a layered approach:

┌─────────────┐
│  Frontend    │  React/Next.js SPA or SSR
├─────────────┤
│  API Layer   │  REST/GraphQL endpoints, auth middleware
├─────────────┤
│  Service     │  Business logic, validation, orchestration
├─────────────┤
│  Data Access │  ORM queries, caching, queue producers
├─────────────┤
│  Database    │  PostgreSQL, Redis, S3
└─────────────┘

Key Decisions

  • Monolith vs Microservices: Start with a modular monolith. Extract services only when you have clear scaling needs or team boundaries. A well-structured monolith with clear module boundaries is faster to develop and easier to debug than premature microservices.
  • State Management: For React frontends, choose between Redux Toolkit for complex global state, Zustand for lightweight stores, or server state tools like React Query/TanStack Query for API caching. TanStack Query eliminates manual cache invalidation with stale-while-revalidate patterns.
  • Authentication: Use JWTs stored in HTTP-only cookies for SPA security. Implement refresh token rotation with short-lived access tokens (15 min) and long-lived refresh tokens (7 days). Store session metadata in Redis for revocation capability.

Example: Express.js API Structure

/src
  /config       → database, redis, env config
  /middleware    → auth, rate-limit, errorHandler, validate
  /routes        → route definitions mapping to controllers
  /controllers   → request/response handling
  /services      → business logic (no req/res objects)
  /models        → database schemas and static methods
  /utils         → helpers, formatters, email templates

This separation ensures testability—services can be unit tested without HTTP context, and controllers stay thin.

Scalable Database Design

Scalable Database Design

Database design decisions made early are expensive to change later. Proper schema design, indexing strategies, and relationship modeling determine whether your app performs at 100 rows or 100 million rows.

Schema Design Principles

  • Normalization vs Denormalization: Normalize to 3NF for write-heavy workloads. Denormalize strategically for read-heavy dashboards using materialized views or computed columns. For example, a products table stays normalized, but a product_summaries view pre-joins category and inventory counts for the browse page.
  • Indexing Strategy: Add composite indexes for common query patterns. A query filtering by user_id and ordering by created_at DESC needs a composite index on (user_id, created_at DESC). Partial indexes reduce size: CREATE INDEX idx_active_users ON users (email) WHERE active = true;

Example: PostgreSQL Schema with Relationships

CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email VARCHAR(255) UNIQUE NOT NULL,
  password_hash TEXT NOT NULL,
  role VARCHAR(20) DEFAULT 'user',
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE projects (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  owner_id UUID REFERENCES users(id) ON DELETE CASCADE,
  name VARCHAR(100) NOT NULL,
  status VARCHAR(20) DEFAULT 'active',
  metadata JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_projects_owner ON projects (owner_id, created_at DESC);
CREATE INDEX idx_projects_status ON projects (status) WHERE status = 'active';

Migration Strategy

Use tools like Prisma Migrate, Knex migrations, or Drizzle Kit to version-control schema changes. Never manually alter production databases. Each migration should be reversible and tested against a staging environment before deployment. Run prisma migrate dev --name add_user_roles to generate timestamped migration files.

Real-Time Features & Event Architecture

Real-Time Features & Event Architecture

Modern full stack apps increasingly rely on real-time updates—live notifications, collaborative editing, instant messaging, or live dashboards. Planning these features upfront prevents bolted-on solutions that introduce bugs.

WebSocket Implementation Pattern

Use Socket.io for production WebSocket handling with automatic reconnection, room-based broadcasting, and middleware support:

// server.ts
import { Server } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();

const io = new Server(httpServer, {
  cors: { origin: process.env.CLIENT_URL, credentials: true },
  adapter: createAdapter(pubClient, subClient)
});

io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  const user = verifyJWT(token);
  if (!user) return next(new Error('Authentication required'));
  socket.data.user = user;
  next();
});

io.on('connection', (socket) => {
  socket.join(`user:${socket.data.user.id}`);
  
  socket.on('project:update', (data) => {
    io.to(`project:${data.projectId}`).emit('project:changed', data);
  });
});

Event-Driven Architecture

For decoupled services, use a message broker like RabbitMQ or Redis Streams:

  • Producer: Emits events like order.created, user.signed_up without knowing who consumes them
  • Consumer: Subscribes to events and performs side effects (send email, update analytics, trigger webhook)
  • Dead Letter Queue: Captures failed messages for retry or manual inspection

Scaling Real-Time

Use Redis adapter for horizontal scaling across multiple server instances. Each instance publishes to Redis pub/sub, and the adapter distributes messages to all connected clients across the cluster. Monitor connection counts and implement graceful degradation with polling fallbacks for unreliable networks.

Authentication & Authorization Systems

Authentication & Authorization Systems

Authentication is the most security-critical part of your application. A single vulnerability can expose all user data. Planning auth architecture carefully prevents common attacks and ensures compliance.

JWT-Based Auth Flow

1. User submits credentials → POST /api/auth/login
2. Server validates password with bcrypt.compare()
3. Server creates access token (15min) + refresh token (7 days)
4. Tokens stored in HTTP-only, Secure, SameSite=Strict cookies
5. Access token verified on every request via middleware
6. Refresh token rotated on use, old token invalidated in Redis

Implementation Example

// authMiddleware.ts
import jwt from 'jsonwebtoken';
import { redis } from './config/redis';

export const authenticate = async (req, res, next) => {
  const token = req.cookies.accessToken;
  if (!token) return res.status(401).json({ error: 'No token provided' });
  
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    const isRevoked = await redis.get(`revoked:${payload.jti}`);
    if (isRevoked) return res.status(401).json({ error: 'Token revoked' });
    
    req.user = payload;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') return res.status(401).json({ error: 'Token expired' });
    return res.status(403).json({ error: 'Invalid token' });
  }
};

// RBAC middleware
export const authorize = (...roles) => (req, res, next) => {
  if (!roles.includes(req.user.role)) {
    return res.status(403).json({ error: 'Insufficient permissions' });
  }
  next();
};

// Usage: router.delete('/users/:id', authenticate, authorize('admin'), deleteUser);

Security Checklist

  • Hash passwords with bcrypt (cost factor 12) or Argon2
  • Implement rate limiting on auth endpoints (5 attempts per 15 minutes)
  • Use CSRF tokens for state-changing operations
  • Validate and sanitize all inputs with Zod or Joi schemas
  • Set secure cookie flags: HttpOnly, Secure, SameSite=Strict, Path=/
  • Store refresh tokens in Redis with TTL matching expiration
  • Log failed authentication attempts for anomaly detection

Quiz

1. Why is a modular monolith preferred over microservices for initial development?

Question 1 options

2. What is the benefit of using composite database indexes for common query patterns?

Question 2 options

3. Why should JWT access tokens be stored in HTTP-only cookies instead of localStorage?

Question 3 options

Flashcards

Question

What is the recommended approach for storing JWTs in SPA applications?

Answer

Store JWTs in HTTP-only, Secure, SameSite=Strict cookies. Use short-lived access tokens (15 min) with refresh token rotation. HTTP-only cookies prevent JavaScript access, blocking XSS token theft.

Question

When should you choose a modular monolith over microservices?

Answer

Choose a modular monolith for initial development because it is simpler to debug, deploy, and evolve. Extract to microservices only when you have clear scaling boundaries, independent team ownership, or different technology requirements per service.

Question

What is the purpose of a Redis adapter with Socket.io in production?

Answer

The Redis adapter enables horizontal scaling of WebSocket connections across multiple server instances. Each instance publishes events to Redis pub/sub, and the adapter distributes messages to all connected clients across the cluster, ensuring all users receive real-time updates regardless of which server they connect to.

Revision Notes

Key Takeaways

  • 1. Start with a modular monolith and clear module boundaries; only extract microservices when team or scaling needs demand it
  • 2. Design database schemas with proper composite indexes matching your query patterns; use partial indexes for filtered queries
  • 3. Store JWTs in HTTP-only cookies with refresh token rotation; never expose tokens to JavaScript
  • 4. Plan real-time features with Socket.io and Redis adapter for horizontal scaling from the start
  • 5. Use Prisma Migrate or similar tools for version-controlled schema changes—never manually alter production databases

Interview Tips

  • Explain the tradeoffs between monolith and microservices with specific examples from your experience
  • Walk through how you would design an auth system end-to-end: registration, login, token refresh, revocation
  • Describe how you would add real-time notifications to an existing REST API using WebSockets
  • Discuss database indexing strategy for a query like 'find all active projects by user, sorted by recent'
  • Explain the difference between authentication and authorization, and how RBAC middleware works

Cheat Sheet

Architecture: Modular monolith → extract services later. Database: Composite indexes for multi-column queries, partial indexes for filtered subsets. Auth: JWT in HTTP-only cookies, 15min access + 7-day refresh tokens, bcrypt cost=12. Real-time: Socket.io + Redis adapter for horizontal scaling. Migrations: Always use tooling (Prisma Migrate, Drizzle Kit), never manual DDL in production.