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.