← Back to Pillars
Pillar 06 Deployment

Write Once, Deploy Anywhere

Same codebase targets SQLite, MariaDB, PostgreSQL, or TimescaleDB. Local filesystem or S3. Node.js HTTP server or AWS Lambda. Switch deployment targets via configuration, no code changes.

Java promised "write once, run anywhere" for programs. That promise was about hardware abstraction. Today's challenge is different: we need the same application to run across deployment environments — from a developer's laptop to a Kubernetes cluster to serverless functions.

The Deployment Matrix

A modern application might need to run in dramatically different contexts:

  • Development — SQLite for simplicity, local filesystem, single process
  • Testing — PostgreSQL in Docker, ephemeral storage, isolated instances
  • Staging — Managed PostgreSQL, S3 storage, container orchestration
  • Production — PostgreSQL cluster, S3 with CDN, auto-scaling
  • Edge — SQLite again, local storage, embedded runtime
  • Serverless — Aurora Serverless, no persistent filesystem, Lambda

Without abstraction, each deployment target requires different code paths, different configurations, different testing strategies. The infrastructure bleeds into the application.

Real-World Use Cases

Developer Experience

Your new developer clones the repo, runs npm start, and has a fully functional local environment in seconds. No Docker setup, no database installation, no environment configuration. SQLite, local files, single process — all configured automatically for development mode.

Gradual Cloud Migration

You're migrating from on-premises to AWS. With deployment abstraction, the migration is configuration changes, not code changes. Start with EC2 running your familiar stack. Move to RDS when ready. Adopt Lambda for specific workloads. The application code never changes.

Multi-Cloud & Hybrid

Regulatory requirements mandate certain data stays in your datacenter while other workloads run in the cloud. Same application, different deployment configurations. The on-prem instance uses local PostgreSQL and NFS; the cloud instance uses RDS and S3. Identical application code.

Edge Computing

Your retail application runs in the cloud for headquarters and on local devices in stores. Store instances use SQLite and local storage, syncing to the cloud when connected. The same codebase serves both deployment models.

How Existing Tools Approach This

Knex.js

Query builder that abstracts SQL dialects. Write queries once, run on SQLite, PostgreSQL, MySQL, or MSSQL. Limited to the database layer — doesn't address storage, compute, or other infrastructure concerns.

Prisma

ORM with multi-database support through provider configuration. Change the provider in your schema, regenerate the client, and you're on a different database. Strong for database abstraction, but file storage and compute are separate concerns.

Serverless Framework

Abstracts serverless deployment across AWS, Azure, Google Cloud. Your serverless.yml defines functions; the framework handles provider-specific translation. Focused on serverless compute, not full-stack abstraction.

Terraform

Infrastructure as code across cloud providers. Abstracts resource provisioning but doesn't address application code. Your Terraform might be portable, but your application still imports AWS-specific SDKs.

Docker + Kubernetes

Containerization provides consistent runtime environments. The same container runs locally, in staging, in production. But the application inside still needs configuration for different databases, storage systems, and service endpoints.

LocalStack

Emulates AWS services locally for development. Write to the real S3 API, test against LocalStack in development. Useful, but it's mocking rather than true abstraction — you're still writing AWS-specific code.

The Symlink Pattern

An Application Operating System uses a simple, powerful pattern: deployment configuration via symlink.

# Development (SQLite, local filesystem)
ln -sf package.development.json package.json
npm start

# Staging (PostgreSQL, S3)
ln -sf package.staging.json package.json
npm start

# Production (PostgreSQL cluster, S3 with CDN)
ln -sf package.production.json package.json
npm start

# Edge (SQLite, local, embedded)
ln -sf package.edge.json package.json
npm start

The application reads package.json at startup. Depending on which target is symlinked, it configures itself for that environment. The platform reads the target and configures itself. No code changes.

"Switch deployment targets via symlink: ln -s package.production.json package.json. The platform reads the target and configures itself. No code changes."

Abstraction Layers

True deployment portability requires abstraction at multiple levels:

Database

Your application uses a database abstraction layer that presents a consistent API regardless of backend. Queries, transactions, migrations — all work identically whether the backing store is SQLite or a distributed PostgreSQL cluster.

File Storage

Files are stored through an abstraction that might target local disk, S3, Google Cloud Storage, or Azure Blob Storage. Upload, download, list, delete — same API, different backends.

Compute

Your application code runs identically as a long-running Node.js process, a containerized service, or AWS Lambda functions. Request handling, middleware, routing — all abstracted from the execution model.

Configuration

Environment-specific values (database URLs, API keys, feature flags) come from a unified configuration system that sources from environment variables, files, or secret managers depending on deployment.

Why This Matters

Without deployment abstraction, you inevitably accumulate infrastructure coupling:

  • AWS SDK imports scattered through business logic
  • PostgreSQL-specific SQL queries that break on other databases
  • Lambda-specific handler signatures that don't work locally
  • Environment variable assumptions baked into code

Each coupling makes migration harder, testing more complex, and development slower. True portability means clean separation between what your application does and where it runs.

The Operating System Parallel

Operating systems have solved this problem for decades. Your program doesn't know if it's writing to an SSD or a network drive — the filesystem abstraction handles it. Your program doesn't know if it's running on a single core or multiple — the scheduler handles it.

An application operating system extends the same principle upward: your application doesn't know if it's on Lambda or Kubernetes, PostgreSQL or SQLite, S3 or local disk. The platform handles it.

Write once, deploy anywhere — not as marketing, but as architecture.