DEV Community

Cover image for The First Seam
Bob Palermo
Bob Palermo

Posted on Originally published at bobpalermo.github.io

The First Seam

Identifying Fulfillment as a boundary is only the first step. Now ZibiWorks needs a safe place to separate it.

This article is part of ZibiWorks, an ongoing fictional modernization case study. The ZibiWorks site is the canonical home of the series, with the full company context, starting architecture, and related installments. Explore the full series at ZibiWorks.

In Installment 03: Rewrite vs. Evolve, ZibiWorks chose evolutionary modernization over a rewrite. In Installment 04: Boundaries and Meaning, we identified the business boundaries that should guide that evolution.

Now we need somewhere to act on those decisions.

Fulfillment is still embedded in the monolith, tightly coupled to ordering and inventory. We know what should be separated. The question is where and how to begin separating it safely.

That place is a seam.

From boundary to seam

The business boundary establishes what must be separated. The technical seam establishes where and how.

Installment 04 did the first half, establishing Fulfillment as a real business capability with its own responsibilities, distinct from Inventory and Order Management. This installment does the second: finding a place where that boundary can be acted on in code without forcing everything around it to change at once.

A seam is a controlled point of substitution, where one implementation can replace another without requiring changes to the consuming business logic.

In Installment 03, ZibiWorks chose to evolve the monolith incrementally rather than replace it wholesale. That approach follows the Strangler Fig pattern: gradually moving responsibilities out of the monolith while the existing application continues to operate.

But to redirect a responsibility, we first need a place where that redirection can happen. Fulfillment doesn't have one. Its logic is still tangled into Ordering, and introducing that substitution point is the work of this installment.

The challenge is that not every technically substitutable point is a good place to make the cut.

Where Fulfillment lives today

Right now, ordering reaches straight past the boundaries Installment 04 identified. When an order is submitted, the same code path reserves inventory and creates the shipment request itself, inline:

public sealed class OrderService
{
    private readonly InventoryRepository _inventory;
    private readonly ShipmentRepository _shipments;

    public async Task SubmitOrderAsync(Order order)
    {
        // ... order validation, pricing, persistence ...

        // Fulfillment, done inline:
        await _inventory.ReserveAsync(order);
        await _shipments.CreateFulfillmentRequestAsync(order);

        // ... confirmation, events, the rest of ordering ...
    }
}
Enter fullscreen mode Exit fullscreen mode

OrderService does not just ask for fulfillment to happen. It knows how fulfillment happens: that an inventory reservation comes first, then a shipment request, and in that order. Those two calls do not even belong to the same context. Reserving stock is Inventory's responsibility; arranging a shipment is work Fulfillment coordinates. Ordering reaches into both directly. There is no seam here, and there is no single place to substitute a new Fulfillment without editing ordering itself. That coupling is the thing standing between ZibiWorks and its first cut.

Not every cut is a good seam

There are several places ZibiWorks could technically insert a substitution point around this code. They are not equally useful, and the differences are the whole architectural question.

1. Repository-level substitution

Put interfaces in front of InventoryRepository and ShipmentRepository. The calls are now swappable, but ordering still orchestrates fulfillment step by step and still reaches into Inventory itself. The seam exposes Fulfillment's mechanics and preserves the existing coupling. We have made the wrong thing replaceable.

2. Legacy-shaped facade

Move the existing logic behind a single facade, but expose a contract that mirrors its internal steps: ReserveInventory then CreateShipment. Ordering no longer calls the repositories directly, but it still knows how fulfillment works and in what sequence. The code has moved; the coupling has not. Replacing the legacy implementation would still mean preserving that sequence or changing the callers.

3. Business-intent contract

Expose what ordering actually needs: RequestFulfillment. Ordering states its intent and says nothing about how inventory or shipment are handled. The contract describes the capability, so any implementation, legacy or extracted, is free to satisfy it differently.

The difference between the second and third is not the facade; a facade can express intent perfectly well. The difference is what the contract is shaped around. A legacy-shaped contract describes the steps the old code takes. A business-intent contract describes what ordering wants to be true. Only the latter gives Fulfillment room to change behind it, which is the point of drawing the boundary in the first place.

A good seam expresses business intent, not legacy implementation mechanics.

An interface can enforce a technical separation, but it doesn't establish where the business boundary belongs. That decision comes from understanding the responsibilities on either side. The seam is how ZibiWorks acts on a boundary it has already reasoned about, so the contract has to reflect that boundary rather than the legacy call sequence.

What makes a seam worth cutting

Business intent is the primary test, but a few other principles decide whether a candidate seam is worth committing to.

Criterion What to look for
Business alignment Represents a meaningful business capability, not an incidental grouping of code.
Narrow surface Few callers need to understand or participate in the transition.
Stable contract Remains meaningful while implementations change.
Substitutability One implementation can replace another without changing consuming business logic.
Model independence The contract carries only the data the capability needs without exposing the caller's domain model.

RequestFulfillment satisfies these criteria. The repository-level alternatives may be technically substitutable, but they preserve the wrong responsibilities and dependencies. The difference isn't whether an interface exists. It's whether that interface establishes a useful boundary.

Creating the seam

With the seam identified, the next decision is what crosses it. Ordering needs to request fulfillment, but Fulfillment shouldn't need to understand Ordering's entire domain model.

That means defining a contract around the business intent and a request containing only the information Fulfillment needs. Once those decisions are made, dependency injection (DI) provides the mechanism for selecting the implementation.

Ordering depends on a contract that states intent, and nothing more. Note what it does not pass: the Order itself. It hands Fulfillment a request carrying only what Fulfillment needs.

public interface IFulfillmentService
{
    Task RequestFulfillmentAsync(FulfillmentRequest request);
}

public sealed class OrderService
{
    private readonly IFulfillmentService _fulfillment;

    public OrderService(IFulfillmentService fulfillment) =>
        _fulfillment = fulfillment;

    public async Task SubmitOrderAsync(Order order)
    {
        // ... order responsibilities ...
        var request = FulfillmentRequest.From(order);
        await _fulfillment.RequestFulfillmentAsync(request);
        // ... remaining order responsibilities ...
    }
}
Enter fullscreen mode Exit fullscreen mode

That request is deliberately small: the items and quantities to fulfill, a destination, an order reference. Not the Order aggregate with its pricing, payment, and customer detail.

A seam can hide implementation mechanics and still leak the caller's domain model. A good contract carries only what the receiving capability needs.

Passing Order across the seam would tie Fulfillment to Ordering's model, so a change to how orders are shaped could ripple into Fulfillment. The seam should decouple both the implementation behavior and the shape of the data that crosses it.

The contract has two implementations, and they are alternatives, not a chain. Today, dependency injection supplies the legacy one; later it can supply the extracted one. Only one is active for a given configuration.

// Today: the existing orchestration, lifted behind the
// seam, still running inside the monolith. Fulfillment
// coordinates the existing process. Inventory retains its
// stock reservation rules, and the existing shipping
// integration remains unchanged. Domain ownership and
// integration boundaries are not redesigned in this step.
public sealed class LegacyFulfillmentService
    : IFulfillmentService
{
    private readonly InventoryRepository _inventory;
    private readonly ShipmentRepository _shipments;
    private readonly LegacyOrderAdapter _adapter;

    public LegacyFulfillmentService(
        InventoryRepository inventory,
        ShipmentRepository shipments,
        LegacyOrderAdapter adapter)
    {
        _inventory = inventory;
        _shipments = shipments;
        _adapter = adapter;
    }

    public async Task RequestFulfillmentAsync(
        FulfillmentRequest request)
    {
        // Load the Order the legacy repositories still
        // require, by order reference. The request carries
        // fulfillment intent, not Ordering's whole
        // aggregate, so the adapter resolves the persisted
        // Order here.
        var legacyOrder =
            await _adapter.LoadLegacyOrderAsync(request);

        await _inventory.ReserveAsync(legacyOrder);
        await _shipments.CreateFulfillmentRequestAsync(
            legacyOrder);
    }
}

// Later: a separately deployed Fulfillment service,
// satisfying the same contract from across a network
// boundary. It owns the Fulfillment responsibilities and
// collaborates with Inventory through a defined contract.
public sealed class FulfillmentApiClient
    : IFulfillmentService
{
    // POST /fulfillment/requests
}
Enter fullscreen mode Exit fullscreen mode

The original repositories expect an Order, but the new contract accepts a smaller FulfillmentRequest. The legacy adapter uses the order reference to retrieve the original Order and supply the input those repositories still require. That compatibility logic stays behind the seam and disappears when the legacy implementation is retired.

The legacy implementation adapts to the new contract, not the other way around.

A clean boundary doesn't require the legacy implementation to be clean. It requires the legacy complexity to remain behind the boundary.

Both implementations must honor the same business contract, including what it means for a fulfillment request to be accepted, even as the execution model changes. During this initial refactoring the underlying business behavior is intended to stay the same: the legacy adapter runs the same inventory and shipment operations against the same data stores, now reached through the new contract.

Which implementation is live is decided in one place, the composition root, by registering the contract against one or the other. Ordering never learns which it got.

OrderService depends on IFulfillmentService, which today resolves to LegacyFulfillmentService inside the monolith (loading the existing Order by reference, requesting stock reservation from Inventory, and arranging the shipment) and later to a FulfillmentApiClient calling an extracted Fulfillment service. The composition root configures which implementation is active; it is not a step in the request.

At runtime, Ordering depends on the contract and reaches whichever implementation is wired in: the legacy one today, an extracted service later. The composition root is configuration, not a step in the request, it selects which implementation satisfies the contract. The dotted paths are the future state, not calls the legacy implementation makes.

What the seam settles, and what it doesn't

The seam gives ZibiWorks a controlled way to change how Fulfillment is implemented without changing Ordering. That is meaningful progress, but it is important to distinguish what has been accomplished from what remains unresolved.

Concern Status Why
Business boundary Established Fulfillment was identified as a distinct business capability in Installment 04: Boundaries and Meaning.
Technical seam Established IFulfillmentService gives Ordering one stable contract through which to request fulfillment.
Implementation substitution Enabled The composition root can select the legacy implementation or a future extracted one without changing Ordering.
Business responsibility ownership Partially settled Fulfillment's boundary is established, but responsibilities involving Inventory and shipping still need refinement.
Service extraction Not yet LegacyFulfillmentService still executes inside the monolith.
Data ownership Not yet The shared database is unchanged. An interface does not establish data ownership.
Distributed communication and consistency Not yet Timeouts, retries, idempotency, partial failures, and cross-service coordination remain migration concerns.
Cutover and rollback Partially enabled Switching implementations is localized, but reversing distributed side effects or data changes takes more than switching a DI registration.

The seam makes the application-level substitution decision reversible. It does not automatically make every consequence of extraction reversible. While LegacyFulfillmentService runs in the monolith against the same tables, pointing Ordering back at it is a single change. Once an extracted service starts writing its own data or causing side effects across a network, rollback stops being a dependency-injection choice and becomes a broader architectural problem of data and consistency. That belongs to later installments of Part III, not here.

Temporary by design

LegacyFulfillmentService and the transition wiring are migration scaffolding. They exist to make the change incremental, not because they belong in the final architecture. As discussed in Installment 03: Rewrite vs. Evolve, temporary code is not waste when it makes a difficult transition safer.

Where this leaves ZibiWorks

ZibiWorks now has a controlled place to begin extracting Fulfillment. Ordering expresses what it needs, the legacy implementation continues to serve that request, and the path to a replacement no longer runs through Ordering's business logic.

But the seam is only a technical mechanism. It doesn't decide which responsibilities belong to Fulfillment or which should remain elsewhere.

Creating a technical seam does not determine ownership. The business boundary does, and the seam only gives ZibiWorks somewhere to act on it.

Next: Domain or Utility?

Inventory reservation illustrates why that distinction matters. Fulfillment needs inventory reserved, but as we established in Installment 04: Boundaries and Meaning, Inventory owns the rules governing stock. Fulfillment requests a reservation; it doesn't own Inventory's business logic.

Shipping presents a different question. Fulfillment needs to arrange delivery through Handle With Care, and potentially other carriers in the future. But does integrating with a carrier belong inside Fulfillment, in a separate business domain, or behind a shared technical capability?

These aren't simply decisions about where to put code. They are decisions about business responsibility, ownership, and reuse. And that's where another common architectural mistake appears: not everything reusable should become a shared service, and not every external integration deserves its own bounded context.

The next installment explores how to distinguish business domains from supporting utilities, and why getting that distinction wrong can recreate the coupling we're trying to remove.


ZibiWorks is fictional. The ideas and opinions expressed throughout the series are my own and do not represent the views, positions, or practices of my employer.

Top comments (0)