Why Interface + Factory? The Java Pattern That Makes Everything Replaceable

Published: (February 7, 2026 at 04:39 PM EST)
6 min read
Source: Dev.to

Source: Dev.to

Sergiy Yevtushenko

The Pattern

Every component — use case, processing step, adapter — is defined as an interface with a static factory method:

public interface ProcessOrder {
    record Request(String orderId, String paymentToken) {}
    record Response(OrderConfirmation confirmation) {}

    Result execute(Request request);

    interface ValidateInput {
        Result apply(Request raw);
    }
    interface ReserveInventory {
        Result apply(ValidRequest req);
    }
    interface ProcessPayment {
        Result apply(Reservation reservation);
    }
    interface ConfirmOrder {
        Result apply(Payment payment);
    }

    static ProcessOrder processOrder(
            ValidateInput validate,
            ReserveInventory reserve,
            ProcessPayment processPayment,
            ConfirmOrder confirm) {
        return request -> validate.apply(request)
                                  .flatMap(reserve::apply)
                                  .flatMap(processPayment::apply)
                                  .flatMap(confirm::apply);
    }
}

Four steps. Each is a single‑method interface. The factory method accepts all dependencies as parameters and returns a lambda implementing the use case. The body reads exactly like the business process: validate → reserve → process payment → confirm.

This isn’t an arbitrary convention. There are three specific reasons this structure exists.

Reason 1: Substitutability Without Magic

Anyone can implement the interface—no framework, no inheritance hierarchy, no annotations.

Testing becomes trivial

@Test
void order_fails_when_inventory_insufficient() {
    var useCase = ProcessOrder.processOrder(
        request -> Result.success(new ValidRequest(request)), // always valid
        req -> INSUFFICIENT_INVENTORY.result(),             // always fails
        reservation -> { throw new AssertionError("unreachable"); },
        payment -> { throw new AssertionError("unreachable"); }
    );

    useCase.execute(new Request("order-1", "tok_123"))
           .onSuccess(Assertions::fail);
}

No mocking framework, no @Mock annotations, no when().thenReturn() chains. The test constructs the exact scenario it needs with plain lambdas.

Stubbing incomplete implementations during development

// Payment gateway isn’t ready yet? Stub it.
var useCase = ProcessOrder.processOrder(
    realValidator,
    realInventoryService,
    reservation -> Result.success(new Payment("stub-" + reservation.id(), Money.ZERO)),
    realConfirmation
);

The team working on inventory doesn’t need to wait for the payment team. Each step is independently implementable.

Reason 2: Implementation Isolation

Each implementation is self‑contained. No shared base classes, no abstract methods to override, no coupling between implementations whatsoever.

Contrast with the typical abstract‑class approach

// The abstract‑class trap
public abstract class AbstractOrderProcessor {
    protected final Logger log = LoggerFactory.getLogger(getClass());

    public final Result execute(Request request) {
        log.info("Processing order: {}", request.orderId());
        var result = doExecute(request);
        log.info("Order result: {}", result);
        return result;
    }

    protected abstract Result doExecute(Request request);
    protected abstract Result validate(Request request);

    // “Shared utility” that every subclass now depends on
    protected Result calculateTotal(List items) {
        // 47 lines of logic that one subclass needed once
    }
}

Every implementation becomes coupled to the base class. Changing calculateTotal forces you to understand every subclass. Adding logging to execute injects it into every implementation, whether appropriate or not. The base class becomes a gravity well—accumulating shared code that creates invisible dependencies between implementations that should have nothing in common.

With interface + factory

There is no shared implementation code. Period. Each intersection between implementations is unnecessary coupling with corresponding maintenance overhead—up to needing deep understanding of two projects instead of one, with zero benefit.

// Implementation A: uses database
static ProcessPayment databasePayment(PaymentRepository repo) {
    return reservation -> repo.charge(reservation.paymentToken(),
                                      reservation.total())
                            .map(Payment::fromRecord);
}

// Implementation B: uses external API
static ProcessPayment stripePayment(StripeClient client) {
    return reservation -> client.createCharge(reservation.total(),
                                               reservation.paymentToken())
                            .map(Payment::fromStripe);
}

These implementations don’t know about each other. They don’t share code or a base class. They share only a contract—the interface—and nothing else.

Reason 3: Disposable Implementation

The subtle point: the factory method returns a lambda (or a local record). It can’t be referenced externally by class name, which encourages composition over inheritance and makes it easy to discard or replace implementations without affecting the public API.

// Example: a one‑off implementation used only in a specific test
var testUseCase = ProcessOrder.processOrder(
    validator,
    inventory,
    reservation -> Result.success(new Payment("test-" + reservation.id(),
                                               Money.of(0))),
    confirmer
);

Because the implementation lives only as a lambda, there’s no accidental reuse or hidden coupling. When the need changes, you simply supply a different lambda or factory method.

TL;DR

  • Interface + factory gives you:
    1. Pure substitutability – any implementation can be swapped in without a framework.
    2. Isolation – implementations don’t share hidden base‑class dependencies.
    3. Disposable, composable implementations – easy to stub, test, and replace.

Adopt this pattern to keep your Java codebase modular, testable, and free from the “gravity‑well” problems that plague traditional inheritance‑heavy designs.

ProcessOrder – A Functional Composition Example

static ProcessOrder processOrder(ValidateInput validate,
                                 ReserveInventory reserve,
                                 ProcessPayment processPayment,
                                 ConfirmOrder confirm) {
    return request -> validate.apply(request)          // this lambda IS the implementation
                              .flatMap(reserve::apply)
                              .flatMap(processPayment::apply)
                              .flatMap(confirm::apply);
}

No Direct Instantiation

There is no code that says new ProcessOrderImpl().
No other code depends on the concrete implementation class.
Because nothing can reference it, the implementation is completely replaceable.

  • The interface is the design artifact.
  • The implementation is incidental.

Why This Matters

You might think this is academic until you need to:

  • Replace a synchronous implementation with an asynchronous one.
  • Swap a database adapter for an API adapter.
  • Add a caching layer around an existing step.
  • Completely rewrite a step’s internals.

In each case:

  1. The interface stays the same.
  2. The factory method signature stays the same.
  3. The implementation (which nothing references) gets swapped out.

Result: No downstream changes, no adapter layers, no “backwards‑compatibility” headaches.

The Compound Effect

Each benefit is valuable on its own, but together they create a system where:

Testing is configuration

Assemble the exact combination of real and stubbed components you need.

  • No mocking‑framework overhead.
  • No “mock everything” test fragility.

Refactoring is safe

Replacing an implementation can’t break other implementations because they don’t share code.

  • The compiler enforces the contract through the interface.

Complexity is bounded

Understanding one implementation requires understanding only that implementation and the interfaces it consumes.

  • No deep class hierarchies.
  • No shared utilities that couple implementations.

Incremental development is natural

  • Stub what isn’t ready.
  • Replace stubs with real implementations one at a time.
  • Each step can be developed, tested, and deployed independently.

When Does This Not Apply?

When there is genuinely one implementation and it always will be—e.g., pure utility functions, mathematical computations, simple data transformations. In those cases a static method is perfectly fine.

The pattern pays for itself whenever there is any possibility of multiple implementations, including the test implementation (which almost always exists).

The Shift

Most Java codebases default to concrete classes. Interface extraction happens later, reluctantly, when testing forces it or when a second implementation appears.

Flip this approach:

  1. Start with the interface. Define the contract first.
  2. Implement later. The implementation follows naturally, and when it needs to change, nothing else does.

The interface is what you design.
The implementation is what you happen to write today.

0 views
Back to Blog

Related posts

Read more »

Constructor in java

What is Constructors? A constructor is a special method in Java that is automatically executed when an object of a class is created. It is mainly used to initi...

Data Types in Java

Data types specify the different sizes and values that can be stored in a variable. Java is a statically‑typed language, so every variable must be declared with...

Exception Handling in Java

Introduction Java exceptions are objects that represent errors or unusual conditions that occur during runtime. They disrupt the normal flow of a program, and...