SaaS Architecture: Monolith, Microservices & API Design

Start With a Monolith

Controversial opinion that isn't: Start with a monolith.

A monolith is a single deployable application containing all your code. It's simpler to:

Companies that started as monoliths and succeeded: Shopify, Stack Overflow, Basecamp, GitHub (initially).

When to extract services:

The Modular Monolith

Best of both worlds: monolith deployment with service-like organization.

Structure:

/src
  /modules
    /users
      routes.ts
      service.ts
      repository.ts
    /billing
      routes.ts
      service.ts
      repository.ts
    /projects
      ...

Rules:

This gives you monolith simplicity with a path to services.

API Design

REST (Most Common)

Resource-based API design. Works for most SaaS.

Conventions:

Best practices:

GraphQL (When It Fits)

Query language for flexible data fetching.

Good for: Mobile apps (bandwidth savings), complex UIs with varying data needs
Overkill for: Simple CRUD apps, small teams

tRPC (TypeScript End-to-End)

Type-safe API without schema definition.

Good for: Full-stack TypeScript apps, rapid development
Consider: Lock-in to TypeScript ecosystem

Event-Driven Patterns

Decouple operations that don't need to be synchronous.

When to Use Events

Simple Pattern: Background Jobs

Use a job queue (BullMQ, SQS, Inngest) for:

Advanced: Event Sourcing

Store events as the source of truth, derive state from events.

Use when: Audit trails matter, complex domain logic, undo required
Avoid when: Simple CRUD, small team, unclear requirements

Data Layer Patterns

Repository Pattern

Abstract database access behind interfaces. Makes testing easier and switching databases possible.

// Repository interface
interface UserRepository {
  findById(id: string): Promise;
  create(data: CreateUserData): Promise;
  update(id: string, data: UpdateUserData): Promise;
}

// Implementation with Prisma, Drizzle, etc.
class PostgresUserRepository implements UserRepository {
  // ...
}

Service Layer

Business logic separate from HTTP handling. Routes call services, services call repositories.

// Route handler
app.post('/users', async (req, res) => {
  const user = await userService.createUser(req.body);
  res.json(user);
});

// Service (business logic)
class UserService {
  async createUser(data) {
    // Validation, business rules
    return this.userRepository.create(data);
  }
}

Architecture Anti-Patterns