Skip to content

Module 6: Design and delivery

Turn the service into a system that another engineer can change and release predictably. Keep it as one deployable application: the goal is clear boundaries and a trustworthy delivery path, not more components.

Learning outcomes

By the end of this module, you should be able to:

  • describe the system's modules and dependency direction;
  • separate domain decisions from transport and storage mechanics;
  • introduce a pattern only when it removes demonstrated change friction;
  • move a small requirement from acceptance criteria through review;
  • run the same automated checks locally and in continuous integration;
  • build, identify, inspect, and smoke-test a versioned artifact;
  • explain where reproducibility and supply-chain trust come from.

Start from the architecture you have

The service is a modular monolith: one application, one database, and multiple internal responsibilities.

composition root
├── CLI adapter ──┐
├── HTTP adapter ─┼→ task and auth operations → SQLite repository
└── migrations ───┘                         ↘ audit logging

The composition root creates concrete dependencies and starts the selected adapter. Keep these dependency rules:

  • domain operations do not import the HTTP framework, CLI parser, or SQL driver;
  • adapters translate their protocol into domain inputs and outcomes;
  • the repository owns SQL and transaction mechanics;
  • authentication establishes identity before task operations run;
  • authorization rules remain enforced at the data-access boundary;
  • configuration is read once near startup, validated, then passed inward.

Folder names are not architecture. Verify the rules by inspecting imports and tracing one request.

Run a change-pressure exercise

Use one small requirement:

A task may have an optional due date. Clients can set, clear, and list by it. Invalid dates are rejected consistently by the CLI and HTTP API.

Before coding, write acceptance examples for:

  • create with and without a due date;
  • invalid date syntax;
  • update and clear;
  • list ordered by due date with undated tasks handled explicitly;
  • migration of existing rows;
  • another user attempting to update the task.

Trace which parts must change. A healthy design should require one domain rule, one schema migration, repository queries, adapter mapping, and tests. If validation must be copied between CLI and HTTP, move that decision into shared domain logic. Do not create a new service or framework abstraction for one field.

Refactor only demonstrated friction

Use these questions before adding an abstraction:

  1. Is the same business decision duplicated?
  2. Does infrastructure code prevent testing an important rule?
  3. Do two adapters need the same operation?
  4. Is a dependency likely to change for a known reason?
  5. Can moving one boundary solve the problem more simply?

A function receiving a repository object is often enough in Python. Do not add an interface, factory, command bus, event bus, or dependency-injection container merely because a pattern catalogue contains one.

Useful patterns already justified by this project include:

Pattern Concrete reason
Adapter CLI and HTTP expose the same operations through different protocols
Repository SQL and transaction details should not leak into task rules
Composition root Construction and configuration need one visible location
Migration Stored schema must evolve in ordered, reviewable steps

Name the pattern after the boundary exists; do not reshape the code to resemble a diagram.

Record one architecture decision

Create a short decision record for keeping the modular monolith:

# ADR: Keep one deployable application

Status: accepted
Context: current workload, team, failure modes, and change pressure
Decision: one process and database with explicit internal boundaries
Consequences: what becomes easier, what remains coupled, known limits
Revisit when: measurable conditions that would invalidate the choice

Good revisit conditions are evidence, such as independently scaling one workload, incompatible availability needs, or ownership by separate teams. “The application is getting large” is too vague.

Define the change workflow

Use the smallest workflow that supports review:

problem → acceptance examples → small branch → implementation and tests
→ local checks → review → required CI checks → merge → versioned artifact

For the due-date change:

  1. state user-visible behavior and failure cases;
  2. identify migration and compatibility risk;
  3. implement the narrowest vertical slice;
  4. review the diff against the acceptance examples;
  5. merge only after automated checks pass;
  6. build the artifact from the reviewed commit.

Keep commits understandable and short-lived. A specific team may require more process, but this project does not need multiple permanent branches or a release committee.

Establish one local check command

Document one command that runs all checks required before review. It should:

  • compile or import application modules;
  • run unit and integration tests;
  • create a database from all migrations;
  • upgrade a fixture from the previous schema;
  • verify migration and schema invariants;
  • run any formatter, linter, or type checker the project has deliberately adopted.

The command must exit non-zero on failure and avoid modifying developer data. CI invokes this exact command rather than reimplementing its steps in pipeline YAML.

Do not add checks merely to make the list longer. Each check should catch a named failure cheaply enough to run on every change.

Make dependencies repeatable

Declare direct runtime and development dependencies separately. Commit the resolver's lock file and use locked installation in both local verification and CI.

Record:

  • supported Python version or range;
  • build backend and version constraints;
  • direct dependency purpose;
  • the command that updates the lock;
  • the review and test process for an update.

A lock file controls selected package versions; it does not guarantee that packages are trustworthy or that every platform produces identical bytes. Review dependency changes and remove unused packages.

Build one artifact

Package the application as a Python wheel with its runtime dependencies declared in project metadata. The wheel should provide the API server and CLI entry points without relying on the repository's working directory.

For each build:

  1. start from a clean checkout of one commit;
  2. install the locked build environment;
  3. run the local check command;
  4. derive one version from the release tag or documented version source;
  5. build the wheel once;
  6. compute and retain its cryptographic checksum;
  7. create a fresh environment from the runtime lock and install that exact wheel without dependency re-resolution;
  8. run CLI and API startup smoke tests.

Associate the artifact with its version, commit, dependency lock, check result, and build environment. The wheel alone does not freeze transitive dependencies; the lock and installation procedure are part of the release inputs. Deployment in later modules should promote this exact artifact rather than rebuild source under the same version.

Try two clean builds with the same inputs and compare checksums and unpacked contents. If the bytes differ because of timestamps or tooling metadata, document the cause. Do not claim bit-for-bit reproducibility until the comparison demonstrates it.

Create the CI pipeline

Keep the pipeline linear until execution time proves parallelism is useful:

checkout exact commit
→ install locked tools and dependencies
→ run project check command
→ build artifact
→ install and smoke-test artifact
→ publish only for an authorized release

Configure pull requests to run through the smoke test. Publishing runs only from the protected release context and only after every prior stage succeeds.

Pipeline requirements:

  • pin the runtime and third-party pipeline components to reviewed versions;
  • use least-privilege job permissions;
  • do not expose publishing credentials to untrusted changes;
  • treat caches only as performance aids, never as required state;
  • retain test results, build logs, checksum, and artifact;
  • cancel or supersede obsolete runs when the platform supports it;
  • require successful checks before merging to the protected branch.

Start without deployment credentials. The first useful pipeline proves that reviewed source produces a tested artifact.

Test migration compatibility

The pipeline should exercise two database paths:

  1. Fresh: empty database through every migration to current schema.
  2. Upgrade: representative previous-version fixture through only newer migrations.

After each path, verify constraints, row counts, task ownership, due dates, sessions, and idempotency records. A migration that completes but loses or reassigns data is a failed migration.

Avoid destructive rollback claims that SQLite migrations cannot safely support. Prefer forward fixes and restore from a verified backup when recovery requires returning data to an earlier shape.

Run failure drills

Introduce each failure on a temporary branch and observe the pipeline:

  • a failing domain test;
  • a migration that violates an existing constraint;
  • an undeclared runtime dependency;
  • an artifact missing its CLI entry point;
  • a publishing step without authorization.

For every case, confirm that the pipeline stops before publication and makes the failed stage understandable. A green pipeline is useful only if its failure modes are trustworthy.

Review delivery quality

Track a small baseline rather than chasing universal targets:

  • time from push to first useful failure;
  • total check duration;
  • rate of flaky reruns;
  • artifact build and smoke-test success;
  • time needed to diagnose a pipeline failure.

Change pipeline structure only when measurements reveal a problem. Parallel jobs, remote caches, reusable workflow layers, and deployment matrices add coordination cost and can wait.

Evidence to keep

Your repository should now contain:

  • a current module and dependency diagram;
  • the modular-monolith decision record;
  • due-date acceptance examples and implementation;
  • one documented local check command;
  • project metadata and committed dependency lock;
  • a minimal CI definition using the same check command;
  • migration tests for fresh and upgraded databases;
  • a versioned wheel, checksum, and clean-install smoke result;
  • notes from the pipeline failure drills.

Completion check

You are ready for the next module when you can:

  • trace the due-date change without finding duplicated domain rules;
  • explain each dependency direction and where objects are constructed;
  • justify every abstraction from observed change or test pressure;
  • reproduce locally the command CI runs;
  • install and run the artifact without the source checkout;
  • map the artifact to its exact source commit and dependency lock;
  • demonstrate that failed checks and migrations prevent publication;
  • distinguish a repeatable build process from proven bit-for-bit reproducibility.

Read only when needed

Continue with Module 7: Production and reliability after the completion check passes.