Skip to content
Software Survivor logo
Published on

Build Around Business Capabilities, Not Vendor Features

Build Around Business Capabilities, Not Vendor Features architecture illustration
Authors
  • avatar
    Name
    Antonio Perez
    Twitter

Most platform failures begin with a category error: the architecture is organized around the tools that were bought instead of the work the business must keep doing.

A commerce business will continue to price products, collect payments, fulfill orders, manage returns, and report financial activity. It may not continue using the same storefront, payment provider, ERP, loyalty vendor, or database. When a vendor's data model becomes the application's data model, a vendor change becomes a rewrite of unrelated business workflows.

The more durable starting point is the business capability. Ask what the organization must be able to do regardless of implementation, who owns the decision, and what result other parts of the system need to rely on. Then place vendor-specific behavior behind a boundary that translates to that stable business meaning.

Start with the capability, not the API

“Create a Shopify order” is an API action. “Accept a customer purchase into the order lifecycle” is a business capability.

The difference matters because the latter exposes the questions architecture must answer:

  • What makes two requests the same business operation?
  • Which system owns the order, inventory commitment, and financial record at each stage?
  • What does a successful result mean if an external request times out?
  • Which changes should callers absorb, and which provider differences are material to the business?

The same test applies outside commerce. A payment capability is not a wrapper around a charge endpoint. It has to describe authorization, capture, void, refund, settlement evidence, and the possibility that a provider accepted a request but the caller did not receive the response.

A useful interface names the decision the caller is making. It is narrow enough to be understood and complete enough to preserve material behavior. A generic execute(data) function hides no complexity; it merely makes every caller learn the vendor contract.

Treat vendors as implementations of a contract

An adapter gives each provider a place for its request shapes, response codes, retry behavior, authentication, and identifiers. The rest of the platform works in business terms.

For example, an order workflow might depend on operations like these:

interface OrderRepository {
  findByExternalId(externalId: string): Promise<Order | null>
  save(order: Order): Promise<void>
}

interface PaymentCapability {
  authorize(input: AuthorizationRequest): Promise<AuthorizationResult>
  capture(input: CaptureRequest): Promise<CaptureResult>
}

These contracts are not trying to make all databases or payment providers identical. They state the operations the workflow needs. A provider that cannot support a required operation should expose that limitation explicitly instead of silently reducing the business process to the least capable vendor.

This containment is what made it possible to move a production application from MongoDB to PostgreSQL without turning the migration into an application rewrite. The repository boundary concentrated storage changes, while resumable movement, reconciliation, synchronization, and rollback handled the operational work the interface could not solve on its own. The details are in the MongoDB-to-PostgreSQL migration case study.

Make ownership explicit before integration work begins

Capability boundaries only help if the business can answer who owns a record and its lifecycle. Without that answer, integration code eventually becomes a collection of competing writes.

For every important concept, establish:

  • The authoritative system for the current state
  • Stable identifiers that survive across systems
  • Which system may create, update, or reverse the record
  • How disagreements are detected and resolved
  • What evidence finance, operations, and support need to trust the outcome

That is why a Shopify-to-ERP integration is not complete when the API call succeeds. The platform needs to distinguish receipt of a webhook, validation of the business input, durable processing, submission to the ERP, confirmation, and later reconciliation. Each stage has a different owner and recovery path. The order-sync architecture shows that lifecycle in detail.

Do not abstract imaginary variation

Capability-oriented design has a cost: contracts, mappings, tests, and operational metadata take more work than a direct SDK call. The answer is not to add an abstraction around every dependency.

Create a boundary when the dependency is likely to change, the behavior is business-critical, or the provider's semantics would otherwise leak widely. Keep direct, ordinary code for a short-lived experiment or a dependency with no plausible replacement value. Refactor when the second real implementation or rule proves that the seam is needed.

The goal is not vendor neutrality as an aesthetic preference. The goal is to preserve options where the business will pay for change.

A practical review question

Pick one revenue-critical workflow and inspect its vocabulary. If its core code is full of vendor object names, endpoint statuses, and provider-specific identifiers, the implementation details are probably defining the architecture. If it uses terms such as order, authorization, fulfillment, inventory reservation, and reconciliation, the business capability has a chance to survive the next vendor decision.

That is the standard: a change in vendor should be an adapter and migration problem, not a reason to rediscover what the business does.

Continue exploring

Continue with the principles, implementation stories, and consulting paths that apply to the same platform problem.

Working through a similar platform decision?

Bring the business capability, constraints, and failure modes. I can help identify the smallest responsible next step.

Discuss Your Platform Challenge