Install
$ agentstack add skill-mickeyyaya-refactoring-skills-migration-patterns Open-source listing, not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Dangerous shell/eval execution.
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Migration Patterns
Overview
Migrating a monolith to services, or moving a legacy database to a new schema, is one of the highest-risk engineering activities. Done wrong it causes data loss, extended downtime, and introduces coupling that is worse than the monolith it replaced. The patterns here provide incremental, reversible migration paths — each one shifts traffic or data gradually, allowing rollback at any step.
When to use: Planning service extraction from a monolith; replacing a legacy component; migrating databases without downtime; reviewing a migration PR for hidden risks.
Quick Reference
| Pattern | Core Idea | When to Use | |---------|-----------|-------------| | Strangler Fig | Proxy routes traffic; new service replaces old routes incrementally | Extracting services from a running monolith | | Anti-Corruption Layer | Adapter translates between old and new domain models | Integrating two bounded contexts with conflicting models | | Branch by Abstraction | Abstract interface first, swap implementation behind it | Replacing an internal library or module in-place | | Expand-Contract | Widen schema before migrating data, narrow after | Database column renames, type changes, normalisation | | Dual-Write | Write to both old and new stores in parallel | Migrating a live database with zero downtime | | CDC Bridge | Capture database change events and replay to new store | Backfilling a new service's datastore from a legacy DB | | Change Data Capture | Stream row-level changes via Debezium or AWS DMS | Event-driven migration, read-model population |
Patterns in Detail
1. Strangler Fig
The Strangler Fig pattern (named after the tree that grows around a host) routes all traffic through a proxy. New functionality is built in the new service; the proxy redirects matching routes there. The monolith shrinks as routes migrate until it can be deleted.
Key phases:
- Deploy a routing proxy in front of the monolith (no traffic change yet)
- Build the replacement service endpoint
- Shadow-test: send traffic to both, compare responses, route users to new service
- Remove the monolith code path when confidence is high
Red Flags:
- New service writes directly to the monolith database — tight coupling survives the migration
- No proxy layer — cutover is a big-bang deployment
- Migrating all routes at once — increases blast radius if something is wrong
- No feature flag or percentage rollout — impossible to roll back without a deploy
TypeScript — Express proxy with route-level cutover:
import express, { Request, Response, NextFunction } from 'express';
import httpProxy from 'http-proxy-middleware';
const FEATURE_FLAGS: Record = {
'orders-service': process.env.FF_ORDERS === 'true',
'inventory-service': process.env.FF_INVENTORY === 'true',
};
const monolithProxy = httpProxy.createProxyMiddleware({
target: process.env.MONOLITH_URL,
changeOrigin: true,
});
const ordersProxy = httpProxy.createProxyMiddleware({
target: process.env.ORDERS_SERVICE_URL,
changeOrigin: true,
});
const router = express.Router();
// Gradually cut over /orders to the new service via feature flag
router.use('/orders', (req: Request, res: Response, next: NextFunction) => {
if (FEATURE_FLAGS['orders-service']) {
return ordersProxy(req, res, next);
}
return monolithProxy(req, res, next);
});
// Everything else stays on the monolith
router.use('/', monolithProxy);
Go — percentage-based traffic split:
package proxy
import (
"math/rand"
"net/http"
"net/http/httputil"
"net/url"
)
type StranglerProxy struct {
monolith *httputil.ReverseProxy
newService *httputil.ReverseProxy
routeWeight map[string]int // percent traffic to new service per route
}
func (p *StranglerProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
weight, ok := p.routeWeight[r.URL.Path]
if ok && rand.Intn(100) = {
P: 'pending',
S: 'shipped',
C: 'completed',
X: 'cancelled',
};
function translateOrder(legacy: LegacyOrderRecord): Order {
return {
id: legacy.ord_id,
customerId: legacy.cust_no,
status: STATUS_MAP[legacy.ord_stat],
totalAmountCents: legacy.tot_amt,
orderedAt: new Date(
`${legacy.ord_dt.slice(0, 4)}-${legacy.ord_dt.slice(4, 6)}-${legacy.ord_dt.slice(6, 8)}`
),
};
}
Java — ACL as a dedicated service layer:
@Component
public class LegacyOrderAntiCorruptionLayer {
private final LegacyOrderRepository legacyRepo;
public LegacyOrderAntiCorruptionLayer(LegacyOrderRepository legacyRepo) {
this.legacyRepo = legacyRepo;
}
public Order translateAndFetch(String orderId) {
LegacyOrderRecord record = legacyRepo.findByOrdId(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
return translate(record);
}
private Order translate(LegacyOrderRecord record) {
return Order.builder()
.id(record.getOrdId())
.customerId(record.getCustNo())
.status(mapStatus(record.getOrdStat()))
.totalAmountCents(record.getTotAmt())
.orderedAt(LocalDate.parse(record.getOrdDt(),
DateTimeFormatter.BASIC_ISO_DATE).atStartOfDay())
.build();
}
private OrderStatus mapStatus(String legacyStatus) {
return switch (legacyStatus) {
case "P" -> OrderStatus.PENDING;
case "S" -> OrderStatus.SHIPPED;
case "C" -> OrderStatus.COMPLETED;
case "X" -> OrderStatus.CANCELLED;
default -> throw new IllegalArgumentException("Unknown status: " + legacyStatus);
};
}
}
Cross-reference: domain-driven-design-patterns — Bounded Context mapping and Context Map patterns.
3. Branch by Abstraction
When replacing an internal component (a library, a module, a data access layer) that is called from many places, big-bang replacement is risky. Branch by Abstraction introduces an interface first, moves all callers to the interface, then swaps the implementation — the migration is invisible to callers.
Key phases:
- Extract an interface over the existing implementation
- Update all callers to depend on the interface (not the concrete class)
- Build the new implementation behind the interface
- Run both implementations in parallel with verification (optional)
- Switch the wiring to the new implementation; delete the old one
Red Flags:
- Replacing the implementation before abstracting callers — still a big-bang swap
- Interface designed around the old implementation's quirks — new implementation inherits debt
- No parallel verification step — regression only discovered in production
- Skipping the deletion step — dead code remains, future engineers must guess which path is live
TypeScript — interface-first replacement of a storage adapter:
// Step 1: Define the abstraction
interface UserRepository {
findById(id: string): Promise;
save(user: User): Promise;
delete(id: string): Promise;
}
// Step 2: Wrap the legacy implementation
class LegacyUserRepository implements UserRepository {
async findById(id: string): Promise {
return legacyDb.query(`SELECT * FROM users WHERE id = ?`, [id]);
}
async save(user: User): Promise {
await legacyDb.execute(`INSERT INTO users ...`);
}
async delete(id: string): Promise {
await legacyDb.execute(`DELETE FROM users WHERE id = ?`, [id]);
}
}
// Step 3: New implementation (Postgres + TypeORM)
class PostgresUserRepository implements UserRepository {
constructor(private readonly orm: DataSource) {}
async findById(id: string): Promise {
return this.orm.getRepository(UserEntity).findOneBy({ id });
}
async save(user: User): Promise {
await this.orm.getRepository(UserEntity).save(toEntity(user));
}
async delete(id: string): Promise {
await this.orm.getRepository(UserEntity).delete(id);
}
}
// Step 4 (optional): Verification shim — reads from new, falls back to legacy, alerts on divergence
class VerifyingUserRepository implements UserRepository {
constructor(
private readonly primary: UserRepository,
private readonly shadow: UserRepository,
private readonly metrics: MetricsClient
) {}
async findById(id: string): Promise {
const [primaryResult, shadowResult] = await Promise.allSettled([
this.primary.findById(id),
this.shadow.findById(id),
]);
if (primaryResult.status === 'fulfilled' && shadowResult.status === 'fulfilled') {
if (JSON.stringify(primaryResult.value) !== JSON.stringify(shadowResult.value)) {
this.metrics.increment('repo.divergence', { method: 'findById' });
}
}
if (primaryResult.status === 'fulfilled') return primaryResult.value;
throw (primaryResult as PromiseRejectedResult).reason;
}
async save(user: User): Promise { return this.primary.save(user); }
async delete(id: string): Promise { return this.primary.delete(id); }
}
Cross-reference: design-patterns-behavioral — Strategy pattern for runtime implementation swapping.
4. Database Decomposition: Expand-Contract
Renaming a column, changing a data type, or splitting a table while the application is live requires a three-phase approach to avoid downtime.
Phases:
- Expand — Add the new column/table; write to BOTH old and new. Old reads still work.
- Migrate — Backfill existing rows from old column to new column. Verify parity.
- Contract — Switch reads to the new column. Remove writes to the old column. Drop the old column.
Red Flags:
- Dropping old column before migrating all readers — runtime errors in unreleased consumers
- No backfill verification step — silent data loss
- Executing all three phases in a single deployment — negates the safety of the pattern
- No rollback script for each phase
Dual-Write during Expand phase (TypeScript):
interface OrderRow {
id: string;
// Expand phase: both columns present
customer_id?: string; // old column (being retired)
customer_uuid?: string; // new column (UUID format)
}
async function saveOrder(order: Order, db: DbClient): Promise {
await db.execute(
`UPDATE orders
SET customer_id = $1, -- old column: keep writing during expand phase
customer_uuid = $2 -- new column: write from day one of expand
WHERE id = $3`,
[order.customerId, order.customerUuid, order.id]
);
}
// After backfill + verification: stop writing customer_id, read customer_uuid only
async function saveOrderContracted(order: Order, db: DbClient): Promise {
await db.execute(
`UPDATE orders SET customer_uuid = $1 WHERE id = $2`,
[order.customerUuid, order.id]
);
}
Go — backfill with batching to avoid table locks:
func BackfillCustomerUUID(db *sql.DB) error {
const batchSize = 500
for {
result, err := db.Exec(`
UPDATE orders
SET customer_uuid = gen_random_uuid()
WHERE customer_uuid IS NULL
LIMIT $1`, batchSize)
if err != nil {
return fmt.Errorf("backfill: %w", err)
}
n, _ := result.RowsAffected()
if n == 0 {
return nil // backfill complete
}
time.Sleep(50 * time.Millisecond) // yield between batches
}
}
Cross-reference: database-review-patterns — Schema migration safety checklist.
5. Change Data Capture and CDC Bridge
Change Data Capture (CDC) reads the database transaction log (WAL in Postgres, binlog in MySQL) and emits a stream of row-level events. This enables the new service to maintain its own datastore without the application writing to two places.
Debezium (Kafka Connect) — key concepts:
- Source connector reads the DB log; no application code change needed
- Publishes INSERT / UPDATE / DELETE events to Kafka topics
- New service consumes the topic and applies changes to its own store
- Offset tracking ensures exactly-once processing with idempotent consumers
AWS DMS patterns:
- Full-load task: one-time snapshot of the source table into the target
- CDC task: continuous replication after full load completes
- Validation task: row-count and data-type checks between source and target
- Use
STOP_TASK_CACHED_CHANGESmode to pause without data loss during cutovers
Red Flags:
- No idempotency key on CDC consumer — replays cause duplicates
- CDC lag not monitored — consumer falls behind, cutover window is unknown
- DDL changes (ALTER TABLE) not handled — Debezium schema registry must be updated
- CDC task started before full-load verification — incremental events applied to incomplete base data
TypeScript — idempotent Debezium event consumer:
interface DebeziumOrderEvent {
op: 'c' | 'u' | 'd' | 'r'; // create, update, delete, read (snapshot)
before: LegacyOrderRecord | null;
after: LegacyOrderRecord | null;
source: { lsn: string; ts_ms: number };
}
async function handleOrderCDCEvent(
event: DebeziumOrderEvent,
repo: OrderRepository,
acl: LegacyOrderAntiCorruptionLayer
): Promise {
switch (event.op) {
case 'c':
case 'r':
case 'u': {
if (!event.after) return;
const order = acl.translate(event.after);
// upsert is idempotent — safe to replay
await repo.upsert(order);
break;
}
case 'd': {
if (!event.before) return;
await repo.delete(event.before.ord_id);
break;
}
}
}
Java — Spring Kafka CDC consumer with idempotency guard:
@KafkaListener(topics = "dbserver1.public.orders", groupId = "orders-migration")
public void consume(ConsumerRecord record) {
DebeziumOrderEvent event = record.value();
String eventKey = record.topic() + ":" + record.partition() + ":" + record.offset();
if (processedEventStore.exists(eventKey)) {
log.debug("Skipping already-processed event: {}", eventKey);
return;
}
switch (event.getOp()) {
case "c", "u", "r" -> orderRepository.upsert(acl.translate(event.getAfter()));
case "d" -> orderRepository.delete(event.getBefore().getOrdId());
}
processedEventStore.mark(eventKey);
}
Cross-reference: event-sourcing-cqrs-patterns — event replay and idempotent projection rebuilding. Cross-reference: message-queue-patterns — consumer group lag monitoring and backpressure.
6. Migration Execution Checklist
Use this checklist before and during any migration:
Pre-migration:
- [ ] Feature flag or percentage rollout gate is in place
- [ ] Rollback procedure documented and tested in staging
- [ ] CDC lag or dual-write divergence monitoring is active
- [ ] All three Expand-Contract phases are separate deployments
- [ ] Load test the new service at production traffic levels
During migration:
- [ ] Monitor error rates on both old and new paths
- [ ] Verify row counts and checksums match between old and new stores
- [ ] Gradually increase traffic (1% → 10% → 50% → 100%)
- [ ] Keep the old code path live for at least one release cycle after full cutover
Post-migration:
- [ ] Delete the old code path and schema (avoid dead code)
- [ ] Remove the proxy/ACL once no traffic flows through it
- [ ] Archive or drop deprecated tables after retention period
7. Migration Anti-Patterns
| Anti-Pattern | Description | Fix | |-------------|-------------|-----| | Shared Database with Cross-Service FK | New service references rows in the monolith DB via foreign key — tight schema coupling survives the migration | Each service owns its data; sync via events or ACL, never FK across service boundaries | | Big-Bang Rewrite | Entire system rewritten and deployed in one release — no incremental validation, catastrophic blast radius | Use Strangler Fig for incremental cutover; big-bang rewrites have a near-100% failure rate for non-trivial systems | | No Routing Layer | N
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: mickeyyaya
- Source: mickeyyaya/refactoring-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.