logo
Back to Blog
Building Scalable SaaS Architecture: Lessons from the Trenches
SaaSArchitectureScalabilityNode.jsDatabase

Building Scalable SaaS Architecture: Lessons from the Trenches

Learn the key architectural decisions that can make or break your SaaS application as it scales from 100 to 100,000+ users.

Profile image 1Profile image 2Profile image 3Profile image 4
Nikola Lalovic
January 15, 2024
4 min read

Building Scalable SaaS Architecture: Lessons from the Trenches

After building multiple SaaS applications that have scaled from zero to thousands of users, I've learned that the architectural decisions you make early on can either accelerate your growth or become major bottlenecks later.

The Foundation: Database Design

Your database is the heart of your SaaS application. Here are the key principles I follow:

Multi-tenancy Strategy

There are three main approaches to multi-tenancy:

  1. Shared Database, Shared Schema - All tenants share the same database and tables
  2. Shared Database, Separate Schema - Each tenant has their own schema
  3. Separate Database - Each tenant has their own database

For most SaaS applications, I recommend starting with approach #1 and migrating larger customers to #3 as needed.

Indexing Strategy

Proper indexing is crucial for performance:

-- Always include tenant_id in your indexes
CREATE INDEX idx_orders_tenant_created ON orders(tenant_id, created_at);
CREATE INDEX idx_users_tenant_email ON users(tenant_id, email);

API Design for Scale

Rate Limiting

Implement rate limiting from day one:

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP',
});

app.use('/api/', limiter);

Pagination

Always paginate your API responses:

app.get('/api/users', async (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = Math.min(parseInt(req.query.limit) || 10, 100);
  const offset = (page - 1) * limit;

  const users = await User.findAndCountAll({
    where: { tenant_id: req.user.tenant_id },
    limit,
    offset,
    order: [['created_at', 'DESC']],
  });

  res.json({
    data: users.rows,
    pagination: {
      page,
      limit,
      total: users.count,
      pages: Math.ceil(users.count / limit),
    },
  });
});

Caching Strategy

Redis for Session Management

const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('redis');
const client = redis.createClient();

app.use(
  session({
    store: new RedisStore({ client: client }),
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: { secure: false, maxAge: 86400000 }, // 24 hours
  })
);

Application-level Caching

const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 600 }); // 10 minutes

app.get('/api/dashboard-stats', async (req, res) => {
  const cacheKey = `dashboard-${req.user.tenant_id}`;
  let stats = cache.get(cacheKey);

  if (!stats) {
    stats = await calculateDashboardStats(req.user.tenant_id);
    cache.set(cacheKey, stats);
  }

  res.json(stats);
});

Monitoring and Observability

Health Checks

app.get('/health', async (req, res) => {
  try {
    // Check database connection
    await sequelize.authenticate();

    // Check Redis connection
    await client.ping();

    res.json({ status: 'healthy', timestamp: new Date().toISOString() });
  } catch (error) {
    res.status(503).json({ status: 'unhealthy', error: error.message });
  }
});

Error Tracking

Use tools like Sentry for error tracking:

const Sentry = require('@sentry/node');

Sentry.init({ dsn: process.env.SENTRY_DSN });

app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.errorHandler());

Deployment and Infrastructure

Docker Configuration

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

USER node

CMD ["npm", "start"]

Environment Variables

Never hardcode configuration:

const config = {
  port: process.env.PORT || 3000,
  database: {
    host: process.env.DB_HOST,
    port: process.env.DB_PORT || 5432,
    name: process.env.DB_NAME,
    username: process.env.DB_USERNAME,
    password: process.env.DB_PASSWORD,
  },
  redis: {
    host: process.env.REDIS_HOST,
    port: process.env.REDIS_PORT || 6379,
  },
};

Security Considerations

Input Validation

const Joi = require('joi');

const userSchema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required(),
  name: Joi.string().min(2).max(50).required(),
});

app.post('/api/users', async (req, res) => {
  const { error, value } = userSchema.validate(req.body);

  if (error) {
    return res.status(400).json({ error: error.details[0].message });
  }

  // Process validated data
});

SQL Injection Prevention

Always use parameterized queries:

// Bad
const query = `SELECT * FROM users WHERE email = '${email}'`;

// Good
const query = 'SELECT * FROM users WHERE email = $1';
const result = await client.query(query, [email]);

Performance Optimization

Database Query Optimization

// Use includes to avoid N+1 queries
const users = await User.findAll({
  include: [
    {
      model: Profile,
      attributes: ['avatar', 'bio'],
    },
  ],
  where: { tenant_id: tenantId },
});

Frontend Optimization

// Implement virtual scrolling for large lists
import { FixedSizeList as List } from 'react-window';

const VirtualizedList = ({ items }) => (
  <List height={600} itemCount={items.length} itemSize={50} itemData={items}>
    {({ index, style, data }) => <div style={style}>{data[index].name}</div>}
  </List>
);

Conclusion

Building scalable SaaS architecture is about making smart decisions early and planning for growth. Focus on:

  1. Database design - Get your data model right from the start
  2. API design - Build APIs that can handle growth
  3. Caching - Implement caching at multiple levels
  4. Monitoring - Know what's happening in your application
  5. Security - Build security in from day one

Remember, premature optimization is the root of all evil, but planning for scale is essential. Start simple, measure everything, and optimize based on real data.


Have questions about SaaS architecture? Feel free to reach out or join our developer community to discuss!

Related Articles

AWSCloudFront

Building a Multi-Brand CDN Architecture: Lessons from Scaling CMS Media Delivery

How we architected and implemented a solution serving CMS media across 7 different brand domains using AWS CloudFront, improving performance, security, and SEO while maintaining zero downtime.

Read More
Full StackReact

Modern Full Stack Development in 2024: The Complete Guide

A comprehensive guide to the modern full-stack development landscape, including the best tools, frameworks, and practices for 2024.

Read More

Found This Helpful?

If you enjoyed this article and want to discuss your project or get personalized advice, I'd love to hear from you.