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
productstable stays normalized, but aproduct_summariesview pre-joins category and inventory counts for the browse page. - Indexing Strategy: Add composite indexes for common query patterns. A query filtering by
user_idand ordering bycreated_at DESCneeds 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_upwithout 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?
2. What is the benefit of using composite database indexes for common query patterns?
3. Why should JWT access tokens be stored in HTTP-only cookies instead of localStorage?
Flashcards
Question
What is the recommended approach for storing JWTs in SPA applications?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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.