Platform Migration Patterns

Production engineering guide for platform migration patterns covering patterns, implementation strategies, and operational best practices.

Platform migration is the process of moving applications, services, and infrastructure from one platform to another—whether from on-premises to cloud, monolith to microservices, legacy CI/CD to GitOps, or a custom internal platform to a standard SaaS offering. This matters when the existing platform no longer meets performance, scalability, or developer velocity goals; when operational overhead exceeds benefits; or when the team seeks to standardize across multiple teams or projects. The success of a platform migration hinges not on the tools used, but on the patterns that guide the shift: how data, configuration, state, and workflows move across boundaries.

Incremental Migration: The Safe Path

Migrate services one at a time, with overlapping lifetimes on both platforms. This pattern is ideal when downtime is costly, teams are distributed, and the new platform is still evolving. The key is maintaining a shared state across platforms: a service running on the old platform must read and write data that the new platform can consume, and vice versa.

Start with a dual-write architecture. When a user creates a new account, the service writes to both the old database and the new one. Use a message broker like Apache Kafka or RabbitMQ to decouple the systems. A service on the new platform subscribes to events from the old platform, ensuring eventual consistency.


kafka-console-consumer.sh --bootstrap-server kafka.example.com:9092 \
  --topic user.created --partition 0 --offset 0

Each message is a JSON event:

{
  "event_id": "5a3f2b1c-d1e2-4a3b-8c9d-12e3f4a5b6c7",
  "event_type": "user.created",
  "timestamp": "2025-04-05T12:00:00Z",
  "data": {
    "user_id": 1024,
    "email": "[email protected]",
    "name": "Alice Smith",
    "created_at": "2025-04-05T11:59:55Z"
  }
}

On the new platform, a service consumes this event and updates its own database:

func handleUserCreated(event json.RawMessage) error {
  var payload struct {
    UserID   int    `json:"user_id"`
    Email    string `json:"email"`
    Name     string `json:"name"`
    CreatedAt time.Time `json:"created_at"`
  }

  if err := json.Unmarshal(event, &payload); err != nil {
    return err
  }

  _, err := db.Exec(
    "INSERT INTO users (id, email, name, created_at) VALUES ($1, $2, $3, $4)",
    payload.UserID, payload.Email, payload.Name, payload.CreatedAt,
  )
  return err
}

The migration is validated when the new platform can serve a request that reads from its own database, while the old platform continues to write and serve. Data drift is detected by comparing last_updated timestamps across both databases. When the drift stabilizes, the team can begin decommissioning the old platform.

Pitfalls

Lift-and-Shift: The Fast Transition

Move entire applications, including code, configuration, and data, from one environment to another with minimal changes. This pattern works when the target platform is highly compatible with the source, and the team wants to reduce risk during migration.

The core mechanism is a platform snapshot—a complete export of the application state. This includes:

# Export database
pg_dump -h old-db.example.com -U app_user -d myapp > myapp.dump

# Export all Kubernetes resources
kubectl get all -n staging -o yaml > staging-resources.yaml

# Build and push Docker images
docker build -t registry.example.com/myapp-api:1.2.0 .
docker push registry.example.com/myapp-api:1.2.0

The target platform applies the snapshot in a single operation. The migration is complete when the new platform serves the first request from the imported data.

Key Considerations

# Mapping script: config.json → environment variables
jq -r '
  .api.port as $port |
  .db.host as $db_host |
  .db.port as $db_port |
  .logging.level as $log_level |
  . | {
    API_PORT: $port,
    DB_HOST: $db_host,
    DB_PORT: $db_port,
    LOGGING_LEVEL: $log_level
  } |
  to_entries |
  .[] | "export \(.key)=\(.value)"
' config.json
pg_restore -h new-db.example.com -U app_user -d myapp myapp.dump

Silent Failures

ERROR: could not connect to database: no such file or directory
  (SQLSTATE 08001)

The root cause is not the database server, but a missing environment variable. The solution: automate secret injection via a script that reads the old config and exports it as environment variables.

Incremental Data Migration: The Stateful Shift

When data is the primary asset, and services depend on consistent state, migrate data in stages. This pattern is essential when the new platform has a different data model—e.g., a relational schema in the old platform and a document store in the new.

Use a data pipeline to move data from source to target. The pipeline consists of:

Each stage runs as a separate process. The pipeline is idempotent: it can restart from any checkpoint.

# Extract: pull all user records since last checkpoint
psql -h old-db.example.com -d myapp -c "
  SELECT id, name, email, created_at, updated_at
  FROM users
  WHERE updated_at > '2025-04-04T00:00:00Z'
  ORDER BY updated_at
" --tuples-only --field-separator=',' > users-2025-04-04.csv

Transform the data using a script:

import csv
import json

with open("users-2025-04-04.csv") as f:
    reader = csv.DictReader(f)
    for row in reader:
        user = {
            "id": row["id"],
            "profile": {
                "name": row["name"],
                "contact": {"email": row["email"]}
            },
            "metadata": {
                "created_at": row["created_at"],
                "updated_at": row["updated_at"]
            }
        }
        print(json.dumps(user))

Load the transformed data into the new platform:

# Load into MongoDB
mongoimport \
  --host new-db.example.com \
  --db myapp \
  --collection users \
  --type json \
  --file users-transformed.json

The pipeline runs continuously, using a last_checkpoint timestamp to avoid duplication.

Failure Modes

Parallel Run: The High-Reliability Pattern

Run the old and new platforms side-by-side, serving the same requests. This pattern ensures zero downtime and allows validation of the new platform under real traffic.

The key is traffic mirroring: every request to the old platform is duplicated and sent to the new platform. The new platform responds to the request but does not serve it; it only logs and processes it.

Use a reverse proxy like NGINX or HAProxy to mirror traffic:

upstream old-api {
  server old-api.example.com:8080;
}

upstream new-api {
  server new-api.example.com:8080;
}

server {
  listen 80;
  location /api/ {
    proxy_pass http://old-api;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;

    # Mirror traffic to new API
    proxy_pass http://new-api;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_cache_bypass $http_upgrade;

    # Use a separate header to mark mirrored requests
    add_header X-Mirrored "true";
  }
}

Each request to the new platform includes a X-Mirrored: true header. The new platform logs these requests and compares them with the old platform’s logs.

Validation and Synchronization

import hashlib
import requests

def compare_responses(old_url, new_url, request_data):
    old_resp = requests.post(old_url, json=request_data)
    new_resp = requests.post(new_url, json=request_data)

    old_hash = hashlib.md5(old_resp.content).hexdigest()
    new_hash = hashlib.md5(new_resp.content).hexdigest()

    return old_hash == new_hash
CREATE TABLE migration_errors (
  id SERIAL PRIMARY KEY,
  request_id VARCHAR(64) NOT NULL,
  old_response TEXT,
  new_response TEXT,
  error_message TEXT,
  timestamp TIMESTAMPTZ DEFAULT NOW()
);

When a mismatch is found, the team investigates whether the difference is due to data, logic, or configuration.

Edge Cases

Decommissioning: The Final Phase

Once the new platform is stable and validated, begin decommissioning the old platform. This is not a single step, but a sequence of actions.

Start by disabling write operations to the old platform. Then, run a final data sync. After that, redirect all read-only traffic to the new platform.

Use a migration readiness dashboard that tracks:

When the dashboard shows stable performance and low error rates, cut over all traffic.

Finally, decommission the old platform:

# Compare row counts
psql -h old-db.example.com -d myapp -c "SELECT count(*) FROM users;" --tuples-only
psql -h new-db.example.com -d myapp -c "SELECT count(*) FROM users;" --tuples-only

If the counts match, the migration is complete. If not, the team investigates the data pipeline for missed or duplicated records.

Common Decommissioning Failures

The pattern is complete when the team can point to a new platform and say: “This is where we do everything.”

This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.