Skip to main content

Module Architecture

·1016 words·5 mins

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-core is 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.