Architecture of Subtraction: Why Boring Tech Wins and How to De-Microservice
"For a decade, software engineering over-indexed on distributed complexity. Small engineering teams split straightforward CRUD applications into dozens of micro"
For a decade, software engineering over-indexed on distributed complexity. Small engineering teams split straightforward CRUD applications into dozens of microservices, chasing hyperscale patterns without hyperscale problems. The results were predictable: excessive network overhead, distributed state inconsistency, brittle deployments, observability sprawl, and ballooning infrastructure costs.
Boring technology—PostgreSQL, SQLite, and modular monoliths—solves identical product requirements with superior reliability, lower latency, and minimal operational overhead. Scalability stems from disciplined domain boundaries inside a single deployable artifact, not from network serialization layers.
Distributed Fallacy and Mathematical Reality
Microservices promise independent scalability and deployment velocity. In practice, they trade local in-memory invariants for distributed failure modes. System availability degrades exponentially as synchronous call chains deepen:
If a request traverses services, each maintaining availability (), total system availability drops below two nines:
Latency compounds across network serialization boundaries:
In a monolithic architecture, and . In-process calls resolve in nanoseconds via pointer dereferencing and CPU caches. Network RPCs introduce socket allocations, TLS handshakes, serialization overhead, kernel context switches, and packet retransmissions.
Neil Gunther's Universal Scalability Law (USL) models concurrency limits and throughput saturation:
Where:
- : Concurrent workers or load factor
- : Contention penalty (waiting on shared locks or serial queues)
- : Crosstalk penalty (coherency delay, inter-node consensus, state synchronization)
Microservices artificially inflate through distributed two-phase commits, saga orchestrations, and cache invalidation chatter. As grows, the quadratic penalty dominates, degrading throughput rapidly past an inflection point. Modular monoliths minimize through shared-memory synchronization and ACID-compliant local transactions.
Boring Stack: PostgreSQL and SQLite as Universal Engines
PostgreSQL replaces multiple specialized distributed infrastructure components:
- Relational storage with ACID guarantees
- Document storage via
JSONBcolumns with GIN indexing - Full-text search via native
tsvectorandtsquery - Queue infrastructure via
SELECT ... FOR UPDATE SKIP LOCKED - Pub/Sub mechanics via
LISTENandNOTIFY
-- Atomic job queue implementation in standard PostgreSQL
CREATE TABLE task_queue (
id BIGSERIAL PRIMARY KEY,
task_type TEXT NOT NULL,
payload JSONB NOT NULL,
status TEXT NOT NULL DEFAULT 'queued',
run_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
locked_until TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_task_queue_fetch
ON task_queue (status, run_at)
WHERE status = 'queued';
-- Worker poll: safe atomic dequeue without distributed locks
WITH next_task AS (
SELECT id
FROM task_queue
WHERE status = 'queued' AND run_at <= NOW()
ORDER BY run_at ASC, id ASC
FOR UPDATE SKIP LOCKED
LIMIT 1
)
UPDATE task_queue
SET
status = 'in_progress',
locked_until = NOW() + INTERVAL '2 minutes'
FROM next_task
WHERE task_queue.id = next_task.id
RETURNING task_queue.id, task_queue.task_type, task_queue.payload;
This single SQL statement replaces external Redis or RabbitMQ dependencies, guarantees zero orphaned jobs during database rollbacks, and consolidates disaster recovery into standard database backup operations.
For read-intensive local storage, embedded engines like SQLite outperform client-server databases. Operating in Write-Ahead Logging (WAL) mode, SQLite executes hundreds of thousands of read queries per second directly from memory-mapped files without network roundtrips:
# Enable optimal performance flags for embedded SQLite
sqlite3 app.db "PRAGMA journal_mode = WAL;"
sqlite3 app.db "PRAGMA synchronous = NORMAL;"
sqlite3 app.db "PRAGMA mmap_size = 268435456;" # 256MB mmap
sqlite3 app.db "PRAGMA cache_size = -64000;" # 64MB cache
Modular Monolith Architecture
A modular monolith enforces isolation through language visibility modifiers, type systems, and package encapsulation rather than network boundaries.
src/
├── modules/
│ ├── billing/
│ │ ├── internal/ # Private tables, business logic, entities
│ │ │ ├── repository.ts
│ │ │ └── service.ts
│ │ ├── public/ # Exposed contracts and DTOs
│ │ │ └── types.ts
│ │ └── index.ts # Explicit public API export boundary
│ ├── identity/
│ │ ├── internal/
│ │ └── index.ts
│ └── inventory/
│ ├── internal/
│ └── index.ts
├── shared/ # DB connection pool, telemetry, primitives
└── main.ts # Dependency injection root and HTTP routing
Architectural rules:
- Modules must never import from another module's
internal/directory. Enforce this via compiler visibility or linting rules. - Direct joins across private tables in separate modules are prohibited. Cross-module data requests must traverse public interface boundaries.
- Asynchronous workflows execute via an in-process event bus or the transactional outbox pattern inside the primary database transaction.
De-Microservicing Playbook
Collapsing distributed services into a unified codebase requires a disciplined four-step migration:
1. Colocate Repositories (Monorepo Convergence)
Move separate service repositories into a unified directory structure. Standardize build tooling, linting configurations, and shared runtime dependency versions. Keep services running as independent processes initially.
2. Introduce In-Process Interfaces
Replace HTTP and gRPC client SDKs with standard interface abstractions. Deploy in-process adapters side-by-side with network clients.
// Public interface definition
export interface OrderService {
createOrder(input: CreateOrderInput): Promise<OrderResult>;
}
// Direct in-process execution adapter
export class InProcessOrderService implements OrderService {
constructor(private readonly internalOrderService: InternalOrderProcessor) {}
async createOrder(input: CreateOrderInput): Promise<OrderResult> {
// Direct memory call - zero serialization overhead
return this.internalOrderService.process(input);
}
}
3. Schema Unification
Consolidate disparate service databases into a single PostgreSQL instance using isolated schemas (identity, orders, billing). Keep cross-schema foreign keys disabled initially to maintain modular isolation.
4. Process Consolidation
Wire the dependency injection root (main.ts) to execute all domain modules inside a single process artifact. Decommission microservice ingress controllers, inter-service service meshes, and RPC proxies.
Safe Legacy Deprecation Without Downtime
Decommissioning legacy services without incidents requires a strangler migration pattern paired with active parity verification.
Phase 1: Dual-Write (Old = Master, New = Shadow)
Phase 2: Historical Backfill & Catch-up
Phase 3: Dark Launch & Read Parity Verification
Phase 4: Inverted Write (New = Master, Old = Shadow/Off)
Phase 5: Code & Schema Deletion (Drop Legacy)
Dual Write and Parity Verification Engine
Run the modern implementation alongside the legacy implementation asynchronously. Track the divergence error rate:
Promote the modern implementation to primary only when across sustained production traffic.
import { DeepEqual } from "./utils/equality";
export class ParityValidator<TInput, TOutput> {
constructor(
private readonly legacyHandler: (input: TInput) => Promise<TOutput>,
private readonly modernHandler: (input: TInput) => Promise<TOutput>,
private readonly telemetryCallback: (matched: boolean, diff?: unknown) => void
) {}
async execute(input: TInput): Promise<TOutput> {
// Execute legacy path as source of truth
const legacyResult = await this.legacyHandler(input);
// Shadow execute modern path in background
queueMicrotask(async () => {
try {
const modernResult = await this.modernHandler(input);
const isMatch = DeepEqual(legacyResult, modernResult);
this.telemetryCallback(isMatch, isMatch ? undefined : { legacyResult, modernResult });
} catch (error) {
this.telemetryCallback(false, { error: (error as Error).message });
}
});
return legacyResult;
}
}
Non-Breaking Schema Deletion (Expand/Contract)
Never drop database columns directly in active production. Execute a three-phase rollout:
- Expand: Add new columns or tables. Deploy application code that reads from legacy columns while dual-writing to both paths.
- Backfill: Execute background batch workers to populate historical data into new columns.
- Contract: Deploy application code that reads and writes solely from modern columns. Drop legacy columns using non-blocking schema operations:
-- Non-blocking drop in modern PostgreSQL
ALTER TABLE users DROP COLUMN legacy_auth_token;
Deletion Hygiene: Tombstones and Dead Code Cleanup
Unused abstractions and abandoned endpoints create permanent operational drag. Enforce strict cleanup mechanics:
- Tombstone Telemetry: Instrument deprecated functions with runtime invocation metrics. If zero calls occur across 14 consecutive production days, delete the function body.
- Feature Flag Eviction: Set hard expiry dates on feature toggles. Active flags older than 30 days must trigger automated CI warnings and priority refactoring tickets.
- Dead Endpoint Pruning: Return HTTP
410 Goneon deprecated routes for 30 days. Log caller identity, notify client owners, and remove route handlers once traffic reaches zero.
Practical Engineering Rules
- Default to Single Deployment Units: Never split a service across a network boundary until vertical scaling limits (e.g., 64-core CPU, memory bandwidth saturation) are demonstrably exhausted.
- Exhaust Database Primitives: Leverage native PostgreSQL features (queues, JSONB, search) before provisioning external distributed infrastructure.
- Enforce Compile-Time Boundaries: Strong domain modularity inside a monolith preserves future extraction options without paying upfront distributed complexity costs.
- Verify Parity and Delete Fast: Validate behavior mathematically in shadow mode, flip traffic switches decisively, and immediately purge legacy code paths.
Engineering maturity is measured by complexity avoided, not complexity managed. Boring technology consistently wins on latency, operational simplicity, and developer velocity.
About the Author
huud
@huud
Systems architect and software engineer building high-performance distributed platforms.