Docs Replacing Components

Replacing Components

No system stays the same forever.

Over time, you will want — or be forced — to replace components:

  • A database driver
  • A persistence layer
  • An authentication mechanism
  • A logging backend
  • A messaging system
  • Even an entire service boundary

The critical question is not whether components will be replaced, but:

Can they be replaced without destabilizing the system?

Plumego is designed to make replacement possible — but only if its principles are respected.


First Principle: Replacement Is an Architectural Concern

Component replacement is not a refactoring trick.

It is an architectural capability.

If replacing a component requires:

  • Touching business logic everywhere
  • Changing handler signatures
  • Modifying unrelated packages
  • Introducing temporary global hacks

Then the system was already tightly coupled.

Plumego’s explicit boundaries exist largely to prevent this.


What Counts as a “Component”

In Plumego systems, components typically include:

  • Infrastructure adapters (DB, cache, message broker)
  • External service clients
  • Authentication / authorization providers
  • Serialization formats
  • Observability backends
  • Transport adapters (HTTP, WebSocket, RPC)

Components are defined by replaceability, not by size.


Why Components Need to Be Replaced

Common drivers include:

  • Performance limitations
  • Cost constraints
  • Operational complexity
  • Vendor lock-in
  • New requirements
  • Regulatory or security changes

Replacement is a sign of system maturity, not failure.


The Golden Rule: Replace Behind an Interface

The most important rule:

Components must be replaced behind explicit interfaces.

In Plumego, this usually means:

  • Interfaces defined in the usecase or application layer
  • Implementations living in infrastructure packages
  • Wiring done at the composition root

Example:

type OrderRepository interface {
	FindByID(ctx context.Context, id string) (Order, error)
	Save(ctx context.Context, order Order) error
}

The interface belongs to the consumer, not the provider.


Step-by-Step Replacement Strategy

A safe replacement follows a predictable sequence.

Step 1: Stabilize the Contract

Before replacing anything:

  • Identify the interface
  • Make implicit assumptions explicit
  • Add tests that describe current behavior

Do not change behavior yet.


Step 2: Introduce a Parallel Implementation

Add the new component alongside the old one.

Example:

repo/
  postgres/
  mysql/

Both implement the same interface.

No production traffic should use the new one yet.


Step 3: Wire Selectively

At the composition root:

  • Add configuration flags
  • Choose implementation explicitly

Example:

var repo OrderRepository
if cfg.UseMySQL {
	repo = mysql.New(...)
} else {
	repo = postgres.New(...)
}

This keeps the switch visible and reversible.


Step 4: Validate in Isolation

Before switching production traffic:

  • Run integration tests against the new component
  • Validate performance characteristics
  • Observe error behavior

Replacement should reduce uncertainty, not increase it.


Step 5: Gradual Cutover

In production:

  • Switch in controlled environments first
  • Use canary or staged deployment
  • Monitor closely

Avoid “big bang” replacements whenever possible.


Step 6: Remove the Old Component

Once confidence is established:

  • Remove old code paths
  • Remove compatibility layers
  • Simplify wiring

Temporary duplication should not become permanent.


Replacing Infrastructure Components

Infrastructure is the easiest place to replace components — if boundaries are clean.

Examples:

  • SQL database → different SQL database
  • Redis → in-memory cache
  • HTTP client → gRPC client

Key discipline:

  • Do not leak driver-specific types upward
  • Translate errors at boundaries
  • Normalize behavior in adapters

If driver types appear in usecases, replacement will hurt.


Replacing Cross-Cutting Components

Replacing cross-cutting components (logging, auth, tracing) is harder.

Guidelines:

  • Keep cross-cutting logic in middleware
  • Inject dependencies explicitly
  • Avoid global registries
  • Avoid static singletons

Middleware is the correct replacement boundary for these concerns.


Replacing a Router or Transport

Replacing routing or transport is rare and risky.

If necessary:

  • Introduce a new entry point
  • Run both in parallel
  • Route traffic explicitly
  • Avoid modifying core lifecycle semantics

Never attempt to hot-swap core execution models.


What Not to Replace Incrementally

Some changes should not be incremental:

  • Domain model rewrites
  • Core invariants changes
  • Semantic meaning of APIs

These require deliberate versioning strategies,
not silent replacement.


Replacement and Testing Strategy

Replacement requires tests at multiple levels:

  • Unit tests for interface contracts
  • Integration tests for new components
  • Regression tests for existing behavior
  • Observability validation in staging

Tests are the safety net that makes replacement possible.


Warning Signs of Poor Replaceability

If you observe:

  • Extensive if old { ... } else { ... } logic scattered everywhere
  • Temporary adapters that never get removed
  • Business logic branching on implementation details
  • Tests that depend on concrete implementations

Then boundaries are eroding.

Address that before replacing anything else.


Replacement vs Refactoring

Refactoring improves code without changing behavior.

Replacement changes how behavior is implemented.

Treat replacement with more caution, documentation, and visibility.


Summary

In Plumego systems:

  • Components are meant to be replaced
  • Replacement happens behind interfaces
  • Wiring makes replacement explicit
  • Changes are staged, not rushed
  • Boundaries protect stability

A system that cannot replace components safely
will eventually be forced into a rewrite.

Plumego’s discipline exists to prevent that outcome.


Next

A natural follow-up topic is:

→ Advanced / Background Processing

This explores how to introduce asynchronous work
without breaking request semantics or lifecycle guarantees.