Introduction#
This document captures the design rules behind dxaws-* modules.
The goal is simple: primitive modules must stay small, predictable, and reusable — even as we build higher-level aggregation services (like dxaws-website) and operational runbooks on top.
At a high level:
- Primitives define capabilities (pure Python APIs with stable contracts)
- Execution adapters perform effects (AWS/boto3, Lambda, dry-run, recording, etc.)
- Orchestrators compose capabilities (CLI workflows, Step Functions, runbooks)
Non-negotiable module rules#
1) Interfaces are immutable (only extendable)#
A dxaws-* module publishes an interface that is treated as a long-lived contract.
Rules:
- Once a public interface exists, it must not change in a backward-incompatible way.
- We may extend interfaces by adding:
- new methods
- new optional parameters (with safe defaults)
- new result fields (when results are structured)
- We do not:
- rename existing methods/parameters
- change semantics of existing behavior
- change existing return types
This applies to:
- manager methods
- provider protocols
- model schemas exposed to callers
If we discover a better API later, we add a new method/versioned entrypoint and keep the old one intact.
Contract and versioning policy#
Every primitive module publishes an API contract that consumers can rely on.
Semantic versioning
- MAJOR: breaking change to a published contract (should be rare)
- MINOR: additive changes only (new methods, new optional parameters, new result fields)
- PATCH: bug fixes and internal refactors that do not change the contract
What counts as a breaking change
- changing the behavior/meaning of an existing method
- renaming or removing a method/parameter
- making an optional parameter required
- changing return types or removing fields that callers may depend on
- tightening validation in a way that breaks previously valid inputs (unless gated behind a new opt-in parameter)
Deprecation
- Deprecated APIs remain supported for at least two MINOR releases.
- Deprecations must be documented and, when possible, include an automated warning path.
Contract tests are required
- Each primitive must include at least one adapter-agnostic contract test suite.
- Providers/adapters must pass the same contract tests (AWS/boto3, dry-run, recording, Lambda-hosted).
This policy is what prevents orchestration layers from forcing churn in the primitives.
2) Dependency direction is one-way#
dxaws-coreis the foundation and depends on nothing else in dxaws.- All other modules may depend on
dxaws-core. - Avoid circular dependencies between non-core modules.
This keeps the ecosystem acyclic and prevents “one small change broke everything” failures.
3) Primitives stay pure Python#
A primitive module owns a narrow capability boundary (DNS, ACM, CloudFront, S3, Organizations, etc.).
Primitive code:
- is ordinary Python
- is testable without AWS credentials
- is deterministic and idempotent where possible
- focuses on intent (what should exist), not execution plumbing
Primitives should not:
- know about the CLI planner/executor
- embed Step Functions concepts
- manage global process state
- require boto3 imports in orchestration logic
This matches the manager / provider split used across dxaws.
Execution adapters#
What an execution adapter is#
An execution adapter is the layer that performs effects.
In practice, adapters are typically providers implementing a protocol, such as an AWS provider that talks to boto3 using shared session plumbing from dxaws-core.
Key rules:
- Managers depend on protocols, not concrete providers
- Providers are intentionally boring: translate method calls into external API calls
- AWS session/client creation lives in
dxaws-core
This structure is what makes modules easy to stub, easy to test, and easy to re-host.
AWS adapters (boto3)#
The default adapter for most primitives is an AWS provider that:
- inherits from
AwsProviderBase - uses an injected
AwsSession - creates boto3 clients via the shared base
This keeps all AWS wiring consistent across modules and avoids every module reinventing session logic.
Alternative adapters (dry-run, recording, Lambda)#
Because managers depend on protocols, we can add alternative adapters without changing the manager API.
Examples:
- Dry-run provider – validates inputs and returns a plan of intended operations
- Recording provider – captures requests/responses for debugging and replay
- Lambda adapter – exposes a primitive operation as a Lambda handler while preserving the same input/output schema
Important: hosting a primitive operation in Lambda is a deployment choice, not an architectural rewrite.
Orchestration#
Orchestration belongs above primitives.
Orchestrators (CLI commands, aggregation services, runbooks) should:
- call primitive manager methods
- supply the correct adapter(s)
- manage ordering, retries, and cross-module composition
- translate user intent into a sequence of capability calls
Primitives remain reusable because they don’t learn about orchestration.
Step Functions orchestration#
Step Functions are a good fit when orchestration needs:
- clear state transitions
- long-running workflows (e.g., DNS-validated ACM certs)
- partial failure recovery
- an execution history suitable for operations
The recommended pattern#
- Step Functions orchestrate capabilities, not raw AWS API calls.
- Each Task state invokes a Lambda that performs one primitive operation using the same primitive input/output models.
- Orchestration logic stays in the state machine, not inside the Lambda.
This keeps:
- Lambdas boring and testable
- state machines readable
- primitive interfaces stable and reusable across CLI + automation
Benefits#
Rock-solid interface contracts#
Once primitive interfaces are treated as immutable, higher-level systems can be built with confidence.
- fewer regressions
- safer refactors
- clear semantic versioning
Runbooks become first-class#
Operational procedures (repair, drift correction, migrations, audits) can be implemented as orchestrations without reaching into primitive internals.
- repeatable operations
- visible execution state
- safe recovery paths
Observability becomes a default, not an afterthought#
Orchestration layers (CLI, Step Functions, runbooks) can emit standardized events to the o11y plane without requiring primitives to know where those events go.
- Step Functions provide execution timelines out of the box
- dxaws can publish structured events to EventBridge
- providers can include consistent identifiers and namespaces
Cleaner boundaries, easier testing#
- primitives test with stubs (no boto3, no credentials)
- adapters can be tested at the integration boundary
- orchestrators can be tested with recorded providers or contract tests
This is the foundation for scaling dxaws without turning it into a framework.
Summary#
- Primitive interfaces are immutable; only additive changes are allowed.
- Primitives are pure Python and never depend on orchestration concerns.
- Execution adapters implement effects (AWS/boto3, dry-run, recording, Lambda).
- Orchestrators compose primitives, and Step Functions are the right tool when workflows need durable, observable state.
With these rules, dxaws modules remain stable building blocks — and higher-level services can evolve quickly without breaking the foundation.