Skip to main content

Providers and Protocols

Why providers exist
#

A recurring problem in infrastructure code is entanglement:

  • business logic knows too much about AWS
  • testing requires boto3, credentials, or mocks everywhere
  • small changes in AWS wiring ripple through the codebase

In dxaws, this is addressed by separating responsibilities:

  • Managers decide what should happen
  • Providers know how to talk to AWS

This separation is intentional and foundational.


The manager / provider split
#

Every dxaws module follows the same basic pattern:

  • A manager class that contains orchestration and decision logic
  • One or more providers that implement the side effects

For example (simplified):

class AcmManager:
    def __init__(self, *, provider: ProviderBase) -> None:
        self.provider = provider

    def request_certificate(self, domain: str) -> str:
        # decide what parameters to use
        # call the provider to do the actual AWS work
        return self.provider.request_certificate(domain=domain)

The manager does not:

  • create boto3 clients
  • manage credentials
  • know which AWS API is being called

That knowledge lives entirely in the provider.


What is a provider?
#

A provider is an object responsible for interacting with an external system.

In practice, most dxaws providers:

  • talk to AWS services (ACM, Route 53, CloudFront, etc.)
  • inherit shared AWS plumbing from dxaws-core
  • expose a narrow, module-specific interface

Providers are intentionally boring. They translate method calls into API calls and return results.


Why not call AWS directly from managers?
#

Calling AWS directly from managers seems simpler at first, but it quickly leads to:

  • difficult-to-test code
  • duplicated AWS session logic
  • hidden coupling between modules
  • managers that are hard to reason about

By pushing all AWS interaction into providers:

  • managers can be tested with simple stubs
  • AWS behavior is centralized in dxaws-core
  • modules remain composable and predictable

Protocols: interfaces without inheritance
#

Providers in dxaws are typed using Protocols from the typing module.

A protocol describes behavior, not inheritance.

from typing import Protocol

class ProviderBase(Protocol):
    def request_certificate(self, *, domain: str) -> str:
        ...

This means:

Any object with a request_certificate(domain: str) method is a valid provider.

The object does not need to inherit from ProviderBase.


Why protocols instead of abstract base classes?
#

Using Protocol has several advantages:

  • no runtime inheritance requirements
  • no ABC boilerplate
  • easy test stubs
  • no multiple-inheritance headaches

Compare a test stub:

class StubProvider:
    def request_certificate(self, *, domain: str) -> str:
        return "arn:example"

This stub can be passed directly into a manager without subclassing anything.

Protocols are enforced by type checkers (like mypy), not at runtime.


AWS providers and dxaws-core
#

Actual AWS providers typically inherit from shared infrastructure in dxaws-core:

class AwsProvider(AwsProviderBase):
    def __init__(self, *, aws: AwsSession) -> None:
        super().__init__(aws=aws, provider_ns="acm")

dxaws-core provides:

  • AWS session handling
  • client creation helpers
  • standardized idempotency tokens
  • consistent naming and logging hooks

This keeps AWS-specific behavior in one place.


Why managers depend on protocols, not AWS providers
#

Managers depend on the protocol, not the concrete AWS provider.

This allows:

  • alternative providers (local, dry-run, recorded)
  • easier unit testing
  • future refactors without changing manager code

In other words:

Managers describe intent. Providers implement effects.


Testing with providers
#

Because providers are protocol-based, testing is straightforward.

A typical test looks like:

class StubProvider:
    def request_certificate(self, *, domain: str) -> str:
        return "arn:test"

manager = AcmManager(provider=StubProvider())
assert manager.request_certificate("example.com") == "arn:test"

No AWS credentials. No boto3 mocks. No fixtures.

This keeps tests fast and focused.


When to add methods to a provider protocol
#

Provider protocols start empty or minimal.

Add method signatures only when:

  • the manager needs a new operation
  • the operation represents a real external interaction

Avoid adding methods “just in case”. Protocols should reflect actual usage, not theoretical capability.


Design philosophy
#

The provider/protocol pattern in dxaws is guided by a few principles:

  • Explicit boundaries beat convenience
  • Structural typing beats inheritance hierarchies
  • Testability is a first-class concern
  • AWS plumbing belongs in one place

This approach scales cleanly as the number of modules grows.


Summary
#

  • Providers handle external side effects
  • Managers contain orchestration logic
  • Protocols define provider behavior without inheritance
  • AWS providers inherit shared plumbing from dxaws-core

Understanding providers and protocols is key to understanding dxaws architecture.