Answers, version 2 — say these out loud

Updated 18 min read source
On this page11
  1. Carried over from version
  2. Python — “modern type hints, async patterns, and idiomatic project layout”
  3. Web frameworks — “FastAPI, Django, or Flask”
  4. APIs — “designing, building, and evolving REST APIs”
  5. Databases — “PostgreSQL or MySQL”
  6. Testing — “unit, integration, and contract tests”
  7. Tooling — “Git, code review, trunk-based workflows”
  8. Event-driven — “Kafka, RabbitMQ, SQS, Pub/Sub”
  9. Delivery — “Docker, Kubernetes, IaC, CI/CD”
  10. Operations — “observability, performance”
  11. Regulated and AI — “regulated environments”, “AI-assisted development”

Answers, version 2 — say these out loud

The questions this ad adds, with each answer written the way you would say it. Short sentences on purpose: they are for speaking, not skimming. Anything in [brackets] is a fact only you know — fill it in before the call.

The three rules. 60 to 90 seconds per answer, in the shape context → options → decision → trade-off. Never stop at “I don’t know” — bridge to the nearest thing you have done. Slower English sounds more fluent, not less.

One-line versions are in Last hour, version 2 — read this, then stop. The self-intro and the questions this ad adds about you are in The call, version 2 — what changes.

Carried over from version 1

These apply unchanged. Read them in version 1’s answers:

Python — “modern type hints, async patterns, and idiomatic project layout”

How do you use type hints in a real codebase?

Mainly as a design tool and a safety net at the boundaries, enforced in CI.

Every public function and every module boundary is annotated — parameters and return types. Inside a function I let inference work.

For seams I use Protocol rather than an abstract base class. A repository or a client interface describes the methods it needs, and anything with those methods fits — including a fake in tests — without inheriting from anything. For data: Pydantic at the edges, where untrusted input arrives, and dataclasses or TypedDict inside. Generics with the 3.12 syntax for containers and helpers, Literal or enums for closed sets of values, and assert_never at the end of a match, so adding a new state fails type checking everywhere it is not handled.

The checker runs in CI — mypy or pyright. On a new project, strict from day one. On an existing one, strictness goes on package by package, so the rule always holds for new code. And Any or a type: ignore needs a reason in review, with the error code.

One thing to remember: hints are not enforced at runtime. Pydantic validates at the boundary; everything inside trusts the types because the checker proved them.

Go deeper: Protocol and structural typing

Which async patterns do you rely on?

Four, mostly.

Structured concurrency first. When tasks can fail, I use TaskGroup rather than gather: if one fails, the siblings are cancelled and I get an ExceptionGroup, instead of the others running on unsupervised. gather is still fine for a few independent reads that are all-or-nothing.

Bounded concurrency. Fetching a thousand URLs means a Semaphore acquired inside the coroutine — not around create_task, where it is released before the work starts and limits nothing. Or a few workers reading from a bounded asyncio.Queue, which gives backpressure too.

Timeouts on everything that waits, with asyncio.timeout() around the block. And cancellation respected: if I catch CancelledError to clean up, I re-raise it.

And never blocking the loop. Sync libraries go to a thread with asyncio.to_thread; CPU work goes to a process. A fire-and-forget task needs a strong reference, or it can be garbage-collected mid-flight and fail silently.

[Your example: async in the Invest Planning FastAPI services — what ran concurrently, and how you bounded it.]

Go deeper: TaskGroup and structured concurrency

How do you lay out a Python project?

One pyproject.toml holds everything: the metadata, the dependencies and dependency groups, and the configuration for Ruff, mypy, pytest and coverage. setup.py, setup.cfg and separate tool config files are legacy.

uv manages the virtual environment and a lockfile that is committed, so CI and every developer install exactly the same versions.

The code goes in a src/ layout — src/myservice/. That stops tests from accidentally importing the working directory instead of the installed package, which hides packaging bugs until deploy.

Inside the package I organise by feature rather than by technical layer: each area has its routes, schemas, service and repository together. Settings come from the environment through one typed settings object, read once at startup. Tests mirror the package, with integration tests that need Docker marked so they can run separately.

And the quality gates live in the repo, not in someone’s head: Ruff for lint and format, mypy, pytest — run by pre-commit locally and by CI as the real gate. Plus a multi-stage Dockerfile that installs from the lockfile.

Go deeper: pyproject.toml and building packages

Web frameworks — “FastAPI, Django, or Flask”

FastAPI, Django or Flask?

I’ve used all three, so I choose by the shape of the service.

FastAPI for a typed API service — async, validation and the OpenAPI spec from type hints, and dependency injection that doubles as the test seam. That is what the Invest Planning services ran on.

Django when the product needs the batteries — the ORM with migrations, the admin, auth. For a data-heavy product with a back office, it saves weeks. Its async support is real at the view level, but the ORM still runs a synchronous driver underneath.

Flask for something small and synchronous, or an existing codebase. It gives you routing and little else, so you choose validation, serialisation and the database layer yourself — fine for a focused service, more work for a big one. [On the Automation Platform: what Flask did there.]

Whichever it is, I keep the framework at the edge: thin handlers, services with the logic, repositories with the queries. Then changing framework is a rewrite of the edge, not of the product.

Go deeper: FastAPI overview

APIs — “designing, building, and evolving REST APIs”

How do you evolve an API without breaking clients?

By making almost every change additive, and making the breaking ones visible before they ship.

Additive is safe: a new endpoint, a new optional field, a new optional parameter. Clients have to be tolerant readers that ignore fields they do not know, and I say so in the docs. What breaks clients is removing or renaming a field, changing its type, tightening validation, or changing the error shape. Those need a new version — in the path, /v2/ — and the old one lives on for a deprecation window.

Deprecation is a process, not a release note: Deprecation and Sunset headers on the old endpoint, a date communicated to clients, and metrics on who still calls it. I remove it when the traffic is gone, not when the date arrives.

What makes this hold is the spec. The OpenAPI document is committed, and CI diffs it against main with a tool that knows breaking from additive — a removed field fails the build, a new optional one passes. Where other teams consume the API, consumer-driven contract tests catch what the spec cannot: a field the spec calls optional that a consumer actually depends on.

Two conventions I fix on day one, because changing them later is breaking: one error format — Problem Details — and an opaque pagination cursor, so I can change what is behind it.

Go deeper: Contract-first API design

Databases — “PostgreSQL or MySQL”

PostgreSQL or MySQL — what’s the difference that matters?

I’ve worked mostly with Postgres, but four differences matter in practice.

Storage. InnoDB stores the table in primary-key order — a clustered index — and every secondary index holds the primary key. So a secondary lookup is two traversals, and a wide or random primary key inflates every index. Random UUIDs as keys hurt MySQL far more than Postgres; UUIDv7 or a sequential key fixes it.

Schema changes. Postgres DDL is transactional, so a migration that fails halfway rolls back cleanly. In MySQL, DDL commits implicitly, so a failed migration can leave the schema half-changed. That changes how carefully you split migrations.

Defaults. Postgres defaults to Read Committed and MySQL to Repeatable Read, so the same code can behave differently under concurrency. And MySQL’s utf8 is a three-byte encoding that cannot store emoji — always use utf8mb4.

Big online changes. Postgres has CREATE INDEX CONCURRENTLY and NOT VALID constraints. MySQL 8 has online DDL for many operations, and for the rest people use gh-ost or pt-online-schema-change, which build a shadow table and swap it in.

Beyond that, Postgres has the richer feature set — JSONB, partial and expression indexes, extensions like pgvector — which is why I default to it.

Go deeper: MySQL

Testing — “unit, integration, and contract tests”

What is contract testing, and when is it worth it?

It checks the agreement between two services without running them together.

In consumer-driven contract testing — Pact is the usual tool — the consumer’s tests record what it actually needs from the provider: these endpoints, these fields, these status codes. That becomes a contract file. The provider’s CI replays the contract against the real provider, so a change that would break a consumer fails the provider’s build — before deploy, not in staging.

It is different from the two things people confuse it with. A schema check verifies the provider matches its own OpenAPI spec — but the spec can be wrong, and it cannot know which optional field a consumer depends on. An end-to-end test runs everything together — slow, flaky, and it finds the problem late.

So it is worth it when services are deployed independently by different teams, and a broken integration is expensive. Inside one team and one repository, a shared schema and integration tests are usually enough.

For a third-party API I cannot run their CI, so the contract becomes recorded responses my tests replay, plus a scheduled job against their sandbox that alerts when their API drifts.

[Your bridge: how the Salesforce and Microsoft Graph integrations on the Automation Platform were tested.]

Go deeper: Integration and contract testing

Tooling — “Git, code review, trunk-based workflows”

How do you work with Git — trunk-based or feature branches?

Trunk-based, with short-lived branches — that is what fits a service that deploys continuously.

A branch lives a day or two, not weeks. The pull request is small enough to review properly — a few hundred lines — with a description that explains why, not just what. Work that is not finished still merges, behind a feature flag, so main stays releasable and nobody carries a long-lived branch that drifts away from it.

main is protected: a required review, required CI checks, no direct or force pushes. I rebase my own branch onto main to stay current and push with --force-with-lease, never plain --force, and I never rebase a branch someone else is building on. Merges are usually squash, so main has one commit per change and reverting is one command.

Why not GitFlow? It suits software shipped in versions — mobile apps, libraries, devices with several supported releases. For a web service it adds merge pain without adding safety.

In a regulated setting there is a bonus: branch protection and CI logs are the change-management evidence an audit asks for, produced automatically.

[Your teams: the workflow on each recent project — GitHub on the Automation Platform, Bitbucket on Invest Planning.]

Go deeper: Branch strategies

Event-driven — “Kafka, RabbitMQ, SQS, Pub/Sub”

Kafka, RabbitMQ, SQS or Pub/Sub — how do you choose?

The first question is queue or log.

RabbitMQ and SQS are queues: a message is delivered, processed, acknowledged and gone. They fit commands and jobs — “process this submission”. RabbitMQ adds rich routing through exchanges; SQS is fully managed and nearly zero-ops, with a visibility timeout instead of an acknowledgement held on a connection.

Kafka is a log: messages stay for the retention period, and each consumer group keeps its own offset. That fits events that many consumers read independently, and replay — a new consumer a year later can read the history. Ordering is per partition, so the partition key decides what stays in order.

Google Pub/Sub sits between them: managed like SQS, with fan-out to many subscriptions, and ordering keys when you need ordering.

Two things hold whichever you pick. Delivery is at-least-once, so consumers must be idempotent. And a service that writes to its database and publishes an event needs a transactional outbox, or a crash between the two loses or invents events.

[Your evidence: SQS and SNS on Invest Planning — what for; Step Functions orchestration on the Automation Platform; Pub/Sub on an earlier project, in one sentence. Kafka from the mechanism rather than production — say so.]

Go deeper: Kafka vs RabbitMQ

Delivery — “Docker, Kubernetes, IaC, CI/CD”

What does a good Python container look like — and what changes on Kubernetes?

A multi-stage build. The first stage installs dependencies from the lockfile — uv makes that fast and cacheable — copying the lockfile before the source, so a code change does not reinstall everything. The final stage is a slim Python base with only the installed environment and the code: no compilers, no build tools, a non-root user, one process per container, configuration from environment variables, and a .dockerignore so secrets never enter the build context.

On Kubernetes, three things matter for a Python service. Probes: liveness answers “is the process alive” and touches nothing else; readiness checks the dependencies needed to serve. A database check in liveness turns a slow database into a restart loop. Requests and limits: requests drive scheduling, a memory limit protects the node, and a CPU limit set too low shows up as unexplained slowness. And graceful shutdown: on SIGTERM, stop accepting, finish in-flight requests, close the pool, with a grace period longer than the slowest request.

I’ve run containers in production on ECS — the Streamlit apps on the Automation Platform — and the principles carry over. Kubernetes adds the probes and the autoscaler, which I’d scale on the metric that reflects load, not just CPU.

Go deeper: Probes and HPA

How does a change get from your branch to production?

Every step is automated, and every step can stop it.

On the pull request, CI runs the cheap checks first — Ruff, mypy, unit tests — then integration tests against real Postgres in a container, and the contract tests. A reviewer approves, and branch protection will not merge without both.

On merge to main, the pipeline builds one image, tagged with the commit, and pushes it to the registry. The same image goes to every environment — staging first, smoke tests, then production — so what was tested is exactly what runs.

The deploy itself is rolling or canary, with health checks gating each step and metrics watched afterwards. Rollback is redeploying the previous image, which only works because database migrations are backward-compatible: expand before the code that needs it, contract in a later release.

Risky behaviour ships dark, behind a feature flag, so deploying and releasing are separate decisions. And infrastructure follows the same path — the plan is shown on the pull request and applied by the pipeline, not from a laptop.

[Your pipelines: GitHub Actions on the Automation Platform — SAM builds and images pushed to ECR; Bitbucket CI/CD on Invest Planning.]

Go deeper: Deployment strategies

Terraform, Pulumi or CloudFormation?

They share one model: declare the infrastructure you want, preview the difference, apply it. The differences are language and state.

Terraform uses HCL, works across clouds, and keeps state in a file you manage — a remote backend with locking, encrypted, because it holds secrets in plaintext. The workflow is plan on the pull request, so reviewers see exactly what will change — especially anything marked for replacement — and the pipeline applies that saved plan.

Pulumi is the same idea in a general-purpose language like Python or TypeScript, so you get loops, functions and tests instead of a DSL. pulumi preview is the plan and pulumi up the apply, and state lives in Pulumi Cloud or a backend you choose. It suits a team that wants infrastructure in the language it already writes.

CloudFormation is AWS-only, but AWS manages the state and rolls back a failed stack automatically.

My hands-on side is AWS SAM — CloudFormation underneath — through the GitHub Actions workflows on the Automation Platform. [How much of the SAM templates you wrote.] For Terraform I know the model and its failure modes: state drift, the replacement of a stateful resource, and applying from a laptop.

Go deeper: CloudFormation vs Terraform

Operations — “observability, performance”

How would you instrument a new service?

With OpenTelemetry, so the instrumentation is vendor-neutral and the backend — Datadog, Grafana, anything that speaks OTLP — is swappable.

Auto-instrumentation covers most of it: incoming requests in FastAPI, outgoing calls through httpx, queries through SQLAlchemy. That gives a trace per request with a span for every hop. I add spans only around the business steps that matter.

Then the three signals, correlated. Metrics for health — rate, errors and duration per endpoint, with labels that have bounded values; a patient id as a metric label explodes cardinality and cost. Structured JSON logs with the trace id on every line, so I can jump from a slow trace to its logs. And errors to Sentry, carrying the same id.

Export goes through an OpenTelemetry Collector, which batches, redacts and samples in one place. I prefer tail-based sampling there: keep every error and every slow trace, plus a small percentage of the rest. Metrics are never sampled, so dashboards stay accurate.

In healthcare one rule overrides the rest: no patient data in spans, logs or error reports. Redaction belongs in the collector and in the Sentry setup, with local variables stripped from tracebacks.

[Your evidence: Sentry on Invest Planning; the observability dashboard on the Automation Platform.]

Go deeper: OpenTelemetry

How do you make a slow endpoint fast?

Measure first, because intuition is usually wrong.

I start from the number that matters — p99 latency or throughput — not the average. Then I find where the time goes: a trace shows whether it is the database, an external call or our own code, and for our own code py-spy attaches to the running process without a restart.

The usual suspects, roughly in the order I find them: an N+1 or a missing index; a blocking call inside async code, which shows up as p99 rising everywhere while the CPU is idle; independent calls made one after another instead of concurrently; a connection pool that is too small, so requests queue; and serialising big payloads.

The fixes follow from what I find: batch or join the queries, run independent I/O concurrently, cache what is read often and changes rarely, move heavy work to a background worker, and paginate. For a hot path in Python itself, a faster JSON library, or moving the computation into NumPy or native code.

Then I prove it with a load test — Locust or k6 — before and after, at realistic concurrency, watching p99. A change that improves the average and worsens the tail is not an improvement.

[Your story: one slow thing you made fast — before, cause, fix, after.]

Go deeper: Async and performance

Regulated and AI — “regulated environments”, “AI-assisted development”

What does working in a regulated environment mean for you as an engineer?

That having controls is not enough — you have to be able to prove them, continuously.

Most of the engineering is the same across HIPAA, GDPR, ISO 27001 and SOC 2: least-privilege access, encryption in transit and at rest, audit logs of who accessed and changed what, reviewed and tested changes, tested backups, and dependencies patched within a set time. What changes is evidence. The good version is controls that produce their own: branch protection and CI logs are the change-management record, the identity provider’s export is the access review, scanner output is the vulnerability record. Anything that depends on someone remembering shows up as a gap, because a SOC 2 Type II audit samples evidence over months.

Audit logging specifically: written in the repository layer or emitted as an event, so no code path can skip it — append-only, and separate from debug logs.

Medical-device software adds a layer, IEC 62304: a software safety class that sets how much rigour applies, traceability from requirements to tests, and every third-party library listed and assessed as SOUP.

My regulated experience is finance: on the Invest Planning Platform, audit and consent records were part of the data model because the advice was regulated. [How they were stored, and who could read them.] It is the same discipline as health data.

Go deeper: SOC 2, ISO 27001 and GRC for engineers

How do you use AI-assisted development tools?

Every day, for the mechanical parts — and with more review, not less.

Where they earn their place: scaffolding and boilerplate, tests from a written specification, unfamiliar APIs, finding my way around a codebase I don’t know yet, and mechanical refactors across many files. Where I don’t lean on them: architecture decisions, security-sensitive code, concurrency and production debugging — exactly where generated code looks right and isn’t.

How I keep it safe: small diffs; asking for the approach before the implementation; and tests I wrote or reviewed first, so the tests are the specification rather than whatever the model decided. Project conventions live in a file the tool reads, so it stops re-arguing style. And the real enforcement is CI — lint, types, tests — because a prompt asking for quality is a request, and a gate is a guarantee.

In healthcare there is one more rule: no patient data, secrets or production payloads in a tool that is not inside the compliance boundary.

Has it made me faster? On mechanical work, substantially. On understanding the problem, not at all — and there is more code to review than before.

[Your tools, by name, and what each is for — plus one you stopped using, and why.]

Go deeper: How do you work with AI tools?