the core logic.
- Implementation: Structure code by domain, not by technical layer.
// src/domains/billing/InvoiceService.ts
// Strict domain isolation; no direct imports from 'src/domains/auth'
import { Database } from '../../infrastructure/db';
import { EventBus } from '../../infrastructure/events';
export class InvoiceService {
constructor(private db: Database, private bus: EventBus) {}
async createInvoice(userId: string, amount: number): Promise<Invoice> {
// Business logic encapsulated
const invoice = await this.db.invoices.create({ userId, amount });
// Decouple side effects via events
await this.bus.publish('invoice.created', { invoiceId: invoice.id });
return invoice;
}
}
Step 2: Implement Postgres-as-Queue
External message brokers (RabbitMQ, Kafka, Redis Streams) add infrastructure cost and operational burden. PostgreSQL provides robust ACID transactions and can serve as a reliable job queue using FOR UPDATE SKIP LOCKED. This eliminates the need for a separate queue service until throughput exceeds database limits.
- Rationale: Zero additional infrastructure cost. Leverages existing database backups and replication. Atomic consistency between business data and job state.
- Implementation: TypeScript worker pattern using
pg.
// src/infrastructure/queue.ts
import { Pool, PoolClient } from 'pg';
export class PostgresQueue<T> {
private readonly tableName: string;
constructor(private pool: Pool, queueName: string) {
this.tableName = `queue_${queueName}`;
}
async init() {
await this.pool.query(`
CREATE TABLE IF NOT EXISTS ${this.tableName} (
id SERIAL PRIMARY KEY,
payload JSONB NOT NULL,
status TEXT DEFAULT 'pending',
locked_at TIMESTAMP,
locked_by TEXT
);
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_status
ON ${this.tableName}(status, id);
`);
}
async enqueue(payload: T): Promise<number> {
const res = await this.pool.query(
`INSERT INTO ${this.tableName} (payload) VALUES ($1) RETURNING id`,
[JSON.stringify(payload)]
);
return res.rows[0].id;
}
async dequeue(workerId: string): Promise<T | null> {
const res = await this.pool.query(
`UPDATE ${this.tableName}
SET status = 'processing', locked_at = NOW(), locked_by = $1
WHERE id = (
SELECT id FROM ${this.tableName}
WHERE status = 'pending'
ORDER BY id
LIMIT 1
FOR UPDATE SKIP LOCKED
)
RETURNING id, payload`,
[workerId]
);
if (res.rows.length === 0) return null;
return {
id: res.rows[0].id,
payload: JSON.parse(res.rows[0].payload) as T,
};
}
async complete(jobId: number): Promise<void> {
await this.pool.query(
`UPDATE ${this.tableName} SET status = 'completed' WHERE id = $1`,
[jobId]
);
}
}
Step 3: Vertical Scaling with Connection Pooling
Bootstrapped applications should scale vertically (increasing CPU/RAM) rather than horizontally (adding nodes) until the cost of vertical scaling exceeds the cost of horizontal scaling. Vertical scaling is simpler and avoids load balancer complexity. However, vertical scaling hits a wall with database connections.
- Rationale: Connection limits are the primary bottleneck for vertical scaling. Implementing connection pooling at the application level or via a proxy (PgBouncer) allows a single database instance to handle high concurrency without exhausting resources.
- Implementation: Use
pgbouncer in transaction mode or configure the Node.js pg pool with strict limits.
// src/infrastructure/db.ts
import { Pool } from 'pg';
// Strict connection limits prevent database overload
export const dbPool = new Pool({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASS,
max: 20, // Limit pool size; PgBouncer handles multiplexing
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// Health check for observability
export async function checkDbHealth(): Promise<boolean> {
try {
const client = await dbPool.connect();
await client.query('SELECT 1');
client.release();
return true;
} catch {
return false;
}
}
Step 4: Automated Cost Observability
Costs must be observable as metrics. Infrastructure costs should trigger alerts before they impact runway. Implement cost tracking per feature flag or environment to detect cost anomalies immediately.
- Rationale: Prevents "bill shock." Enables data-driven decisions on architectural changes.
- Implementation: Integrate cost metrics into the monitoring dashboard. Tag all cloud resources with
team:bootstrapped and env:prod.
Pitfall Guide
-
Premature Horizontal Scaling: Creating Kubernetes clusters or auto-scaling groups for traffic volumes that a single VPS can handle.
- Impact: Increases complexity, cost, and deployment time without measurable benefit.
- Fix: Monitor CPU/Memory utilization. Scale vertically until the instance cost exceeds $200/month, then evaluate horizontal scaling.
-
Ignoring Egress Fees: Architecting data flows that move large payloads between regions or services unnecessarily.
- Impact: Egress fees can exceed compute costs by 10x.
- Fix: Co-locate services in the same region. Use CDN for static assets. Compress API responses.
-
Database Connection Leaks: Failing to release database connections in error paths or long-running tasks.
- Impact: Database becomes unresponsive; application crashes with connection timeouts.
- Fix: Use
try/finally blocks for connection release. Implement pool monitoring. Use PgBouncer.
-
Manual Scaling Triggers: Relying on manual intervention to increase resources during traffic spikes.
- Impact: Downtime during spikes; developer burnout from pager duty.
- Fix: Implement automated scaling policies based on CPU or request count. Use serverless options that scale automatically.
-
Over-Optimizing Caching: Implementing complex multi-level caching strategies (Redis, CDN, In-Memory) before database queries are optimized.
- Impact: Cache invalidation bugs; stale data; unnecessary infrastructure cost.
- Fix: Optimize database indexes and query plans first. Add caching only for read-heavy endpoints with high cost-per-query.
-
Skipping Automated Backups: Assuming managed databases provide sufficient protection without verification.
- Impact: Data loss due to accidental deletion or corruption; inability to recover.
- Fix: Verify backup retention policies. Implement point-in-time recovery testing. Store backups in a separate region if critical.
-
Coupling Infrastructure to Code: Hardcoding infrastructure endpoints or credentials in application code.
- Impact: Difficult environment swaps; security risks; deployment friction.
- Fix: Use environment variables. Abstract infrastructure access behind interfaces. Use dependency injection.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Traffic Spike < 10x | Vertical Scale + CDN | Simplest path; absorbs load without architectural change. | Low; predictable per-hour cost. |
| Database Load > 70% | PgBouncer + Read Replica | Connection pooling mitigates concurrency; replica offloads reads. | Medium; replica cost scales with size. |
| Async Processing Needs | Postgres Queue | Zero infra cost; ACID guarantees; easy migration path. | None; utilizes existing DB capacity. |
| Multi-Region Requirement | Edge Functions + Distributed DB | Reduces latency; complies with data residency. | High; requires complex sync logic. |
| Budget Constraint < $100/mo | Serverless + Managed DB | Pay-per-use model; scales to zero; no idle costs. | Variable; correlates with usage. |
Configuration Template
Copy this docker-compose.yml for a local development environment that mirrors a scalable production stack. Includes Postgres, PgBouncer, and the application with health checks.
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:pass@pgbouncer:6432/db
depends_on:
pgbouncer:
condition: service_healthy
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
pgbouncer:
image: edoburu/pgbouncer
ports:
- "6432:6432"
environment:
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=db
- DB_USER=user
- DB_PASS=pass
- POOL_MODE=transaction
- MAX_CLIENT_CONN=100
- DEFAULT_POOL_SIZE=20
depends_on:
- postgres
healthcheck:
test: ["CMD", "pgbouncer", "-V"]
interval: 10s
timeout: 5s
retries: 3
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_DB=db
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
volumes:
- pgdata:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "5433:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d db"]
interval: 5s
timeout: 5s
retries: 5
volumes:
pgdata:
Quick Start Guide
-
Initialize Project:
mkdir bootstrapped-app && cd bootstrapped-app
npm init -y
npm install typescript pg express
npx tsc --init
-
Setup Infrastructure:
Create docker-compose.yml using the template above. Create init.sql to set up the queue table schema. Run docker compose up -d.
-
Configure Environment:
Create .env file with DATABASE_URL=postgres://user:pass@localhost:6432/db. Ensure the application uses the PgBouncer port.
-
Verify Scalability:
Implement the PostgresQueue class from the Core Solution. Run a load test using autocannon or k6 targeting the enqueue endpoint. Monitor database connections via pg_stat_activity to verify pooling behavior.
-
Deploy:
Push to repository. Trigger CI/CD pipeline. Verify deployment to a single VPS or serverless platform with automated backups enabled. Set cost alerts immediately.