Uncategorized

Stand Up a Developer Platform Before You Reach 30 Engineers

A tech lead with 15–30 engineers does not need a repository-wide modernization program to make the next risky change safer. I would build one change lane: a repeatable way to test, review, and release changes to a single production path. It is a deliberately narrow intervention. If it works, the team can copy it; if it fails, the team has learned without making every pull request pay the cost.

The first lane should follow a write path, not a team boundary

Pick a path that engineers expect to change soon and that has an observable result. For this walkthrough, use PATCH /accounts/{id}/billing-address in an existing application. It accepts an address, updates PostgreSQL, returns an account representation, and writes an audit event. Those details make it a better first candidate than a utility module: a test can check the response, the stored value, and the side effect.

Start with a short audit rather than a new architecture diagram. Read the last 20 pull requests touching this endpoint; that is a starting sample size to adjust, not a statistical threshold. Note which files changed together, which reviewers found defects, which checks caught them, and which changes needed follow-up fixes. Then trace one request through the route, validation, database transaction, and audit-event writer. Put the file paths and findings in a short document beside the code. A future reviewer should be able to answer “where does this write happen?” without repeating the investigation.

I would challenge the broad diagnosis in At 30 engineers, CI and code ownership fail before runtime: a repository-wide ownership fix is the wrong first move if only one path has a known change coming, because it adds review friction before the team knows which check prevents a defect. That does not make ownership optional. It makes ownership something to establish from the actual dependency path rather than from the current team chart.

Write down the behavior you can observe today, including behavior you dislike. Record the HTTP status and response shape for a valid update, an invalid postal code, and an unknown account. Check whether a repeated request produces another audit event. Do not “correct” those behaviors while taking the baseline: changing them at the same time would make a failing test ambiguous. OpenAPI 3.1 can describe the endpoint once the team has confirmed its behavior, but the first evidence should come from running the application against a disposable database.

A real database test earns the right to become a gate

Create tests/contract/ with a test that calls the application through its HTTP interface. Run it against PostgreSQL 16 in Docker Compose v2, assuming that major version matches production; if production uses another major version, use that one because database behavior is part of this contract. Seed one account before each test and clear the database afterward. With pytest 8 and psycopg 3, assert the response and query the stored address directly. For the retry case, query the audit table too. This is an executable boundary test, not a mock proving that a mock was called.

Keep the first assertions small. A successful update should return the expected account ID and normalized address, persist that address, and create the expected audit record. An invalid input should leave both tables unchanged. If the current endpoint duplicates audit records on retry, capture that fact in a test marked as an expected failure and open a separate fix; silently changing idempotency during test setup would obscure whether the lane can detect regressions.

Make the local command work before adding CI: docker compose up -d db, followed by python -m pytest tests/contract -q. Put pytest 8 and the application’s test dependencies in requirements-test.txt. Once a fresh checkout can run that command, add this GitHub Actions workflow at .github/workflows/change-lane.yml:

name: change-lane
on: [pull_request]
jobs:
  contract:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: python -m pip install -r requirements-test.txt
      - run: python -m pytest tests/contract -q

The workflow is a runnable starting point, but a test that needs PostgreSQL also needs the database started in CI. Add the same Compose startup used locally before the pytest step, with the repository’s existing Compose file, and wait for its health check rather than inserting a fixed sleep. Keep Python 3.12 only if it matches the supported application runtime. The useful outcome is not a green badge; it is the same command, dependencies, and database behavior on a laptop and on a pull request.

Require one meaningful check before requiring more reviewers

After the test has caught a deliberately introduced failure—a removed audit write or an unpersisted address—add the job to a GitHub repository ruleset as a required status check. This failure drill matters because an always-green test can block merges without protecting the path. Set an initial CI budget of 8 minutes as a value to tune against your runners and database startup time. If the lane regularly exceeds it, inspect startup and fixture costs before splitting the assertions across more jobs.

Now add a narrow CODEOWNERS entry for the route, its write service, and the contract tests. Assign a small group that can explain the transaction and audit behavior, rather than every engineer who has edited those files. Use the GitHub ruleset to require code-owner review for protected branches. One approval is a sensible initial setting to revisit: it establishes a named reviewer without making two people wait for the same scarce specialist. Specify a backup reviewer and a handoff date in the path document so that leave does not turn ownership into a merge outage.

I would not turn on code-owner approval for the entire repository yet, because unrelated changes would acquire a new queue before this lane has shown that its review catches path-specific mistakes. Nor would I use coverage.py 7’s aggregate percentage as the gate. A higher repository coverage number can coexist with an untested database side effect; the retry and persistence assertions are closer to the failure this lane is meant to prevent. Coverage can still show which branch the new tests exercise, without becoming a substitute for those assertions.

For two weeks, record the lane’s actual results: time from pull-request opening to a passing check, time waiting for review, flaky reruns, and defects found before merge. Those are measurements from your own workflow, not universal targets. If review wait dominates, adding tests will not solve the bottleneck; if failures are flaky, making the check required before stabilizing its fixtures will teach engineers to distrust it.

Characterization tests beat a contract platform for this first boundary

There are two plausible ways to formalize the endpoint. pytest characterization tests win when the caller and provider live in the same repository and the main uncertainty is what the database write actually does. They cost database setup and can run more slowly than isolated tests, but they exercise the behavior the team is about to change. Pact consumer-driven contracts win when independently deployed consumers need to agree with a provider on request and response compatibility. They cost consumer contract maintenance, provider verification, and coordination over contract publication; they also will not, by themselves, prove that an address was persisted or an audit event was written.

Use pytest for this lane unless an independently deployed consumer is already part of the change. Keep an OpenAPI 3.1 description for the HTTP surface if the team maintains one, and update it when observed behavior changes. Do not generate a large schema from assumptions before the first test runs: a precise description of the wrong behavior gives reviewers false confidence. If the endpoint later serves another deployable application, its verified examples provide a starting point for Pact rather than being thrown away.

The same restraint applies to refactoring. I agree with Evolve a Production Codebase Without a Full Rewrite that production code should keep serving requests during change, but I would postpone extracting a new service until this lane can detect a behavior change, because extraction otherwise moves an unverified transaction across a deployment boundary. A small internal seam is enough: keep the route stable, isolate the write operation behind a function, and let the database test observe the result.

One protected change proves the setup works

Choose the next real billing-address request as the trial, not a demonstration-only pull request. Have its author run the local command, change the smallest necessary code path, and open a pull request. The owner reviews the response, persisted row, and audit assertion alongside the implementation. Before merging, confirm that the required CI job ran on that pull request and that a failure cannot be bypassed through a different branch or an unprotected merge route.

After deployment, compare the endpoint’s error count and latency with its own pre-change baseline using existing Prometheus metrics or OpenTelemetry traces. A 7-day lookback is an initial comparison window, not a promise that seven days captures every traffic pattern. Check audit-event counts for unexpected duplication as well. Assign one person to inspect those signals and record when the check is complete; “we will watch production” is not an action unless someone owns the watch.

If the trial exposes an untestable dependency, a slow fixture, or an ownership gap, fix that obstacle before copying the lane. If it passes cleanly, select the next write path by upcoming change risk, not by how neatly it fits a proposed service boundary. Repetition should earn a shared template; a template should not dictate the first experiment.

Tomorrow, pick one scheduled change to a write endpoint and trace it from request to stored result. Ask its likely reviewer to help list three behaviors a test must observe, then get one of those tests running locally against a disposable database. That small, verifiable start gives the team something concrete to protect—and a reason to add a gate only after it has proved what it catches.