Principle 1
Build Around Business Capabilities
Business capabilities usually outlive their implementations. A commerce business will still need to authorize payments, calculate prices, redeem loyalty value, manage inventory, and persist orders five years from now. It may not use the same payment provider, commerce platform, database, cloud, or application framework.
That difference should be visible in the architecture. Core code should describe what the business does. Integration code should describe how a particular vendor does it.
For example, checkout code should depend on an interface such as authorizePayment, not on a
provider SDK's charge object. The business operation has stable inputs: an amount, currency,
payment method reference, order reference, and idempotency key. Its result has stable business
meaning: approved, declined, indeterminate, or failed. Stripe, Braintree, or another provider can
implement that contract through an adapter.
The adapter pattern is useful because it creates a deliberate translation boundary. Vendor request objects, status codes, error types, and retry rules remain inside the adapter. The rest of the system works with terms the business understands. This is not abstraction for its own sake. It limits the number of files that must change when a vendor changes its API, when a contract is renegotiated, or when the business adds a second provider.
Stable business interfaces should be narrow enough to remain meaningful and rich enough to expose
important behavior. A generic execute(data): any interface technically hides a vendor, but it
does not create a useful boundary. Neither does copying every field and method from a vendor SDK
into an interface with a different name. The interface should model the capability that callers
need, including failure semantics.
Examples include:
A Shopify adapter can expose catalog publication, order retrieval, fulfillment updates, and refunds without allowing Shopify webhook shapes or GraphQL response types to spread through the orchestration layer.
A payment interface can normalize authorization, capture, void, refund, and settlement status while preserving provider transaction IDs for reconciliation and support.
A loyalty interface can represent balance lookup, earn, redeem, and reversal operations while keeping provider-specific member identifiers and error codes at the boundary.
A repository can express operations such as
findOrderByExternalIdorsavePromotionRedemptionwhile MongoDB documents, PostgreSQL rows, indexes, and transaction APIs remain implementation details.
Vendor abstraction does not mean pretending all vendors are identical. Differences that affect the business must remain explicit. If one payment provider supports separate authorization and capture while another does not, the interface must either represent that distinction or reject unsupported workflows clearly. A lowest-common-denominator interface can be as damaging as direct vendor coupling because it hides capabilities the business actually depends on.
The same caution applies to databases. A repository boundary is valuable around business aggregates and transaction behavior. A universal storage interface that attempts to make SQL, document storage, caches, and search engines interchangeable usually removes the useful features of each. Abstract the dependency at the level where replacement is plausible and valuable. Do not build a private database framework.
The tradeoff is additional code and translation. Adapters require contracts, mapping, tests, and a place to retain provider-specific metadata. For a short-lived experiment, direct SDK usage may be the correct choice. For checkout, payments, loyalty, tax, fulfillment, or other capabilities likely to survive multiple vendor contracts, the translation cost is usually much lower than the eventual migration cost.
Case study: MongoDB to PostgreSQL migration
How Adapter Boundaries Enabled a Zero-Downtime MongoDB-to-PostgreSQL Migration shows where repository boundaries contained the change, where MongoDB assumptions still leaked, and why data movement, reconciliation, synchronization, cutover, and rollback remained separate operational concerns.