Architecture Patterns with Python: the book that keeps integration code alive after you leave the client
Harry Percival and Bob Gregory wrote it for Python developers who have suffered through a tangled application. Any FDE who has handed code over to a client's team has been in exactly that position.

In brief
- The book is free to read on cosmicpython.com and assumes no prior knowledge of DDD
- Three ideas worth keeping: the web is just one entry point, Unit of Work groups operations into one block, and outbound calls must go through an outbox
- The exercises are hard but worth doing, especially alongside a real project
An entire book on software architecture is sitting online, free. The site cosmicpython.com lets you read all of Architecture Patterns with Python under a Creative Commons licence, with a link to O’Reilly Learning.
The authors are Harry Percival and Bob Gregory. They wrote the book while working together at MADE.com, a European e-commerce company that sold furniture online.
Their intended reader is the Python developer who has struggled with code that keeps getting messier, and the preface states plainly that you need no knowledge of DDD or the classic architectural patterns.
For an FDE, the book hits the most painful spot in the job. Writing integration code that works is not hard; the hard part is keeping that code changeable once you have left the project. The authors name the problem in the preface: the bigger the project, the less the most obvious approach helps you manage complexity.
The mud starts with the first endpoint
The Introduction frames the problem with a blunt observation: chaos in software is so common that engineers have a name for it, the “Big Ball of Mud” antipattern.
Picture a typical deployment. You write a Flask route that takes a request, reads the database, pushes data to the client’s CRM and commits. The demo works.
# app.py — before: everything lives in the route
@app.route("/orders/<order_id>/sync", methods=["POST"])
def sync(order_id):
order = db.session.get(Order, order_id)
crm.push(order.to_dict())
order.synced = True
db.session.commit()
return {"ok": True}
In week three, the client wants a nightly job to resync all the data, and you copy the four middle lines into a script. Now there are two copies of the logic, and they will drift apart from the first change.
Idea one: the web is just one entry point
Chapter 4 answers exactly that situation with a service layer. In the authors’ account, all orchestration logic lives in the use-case layer, while business logic stays in the domain. Orchestration here means fetching data from the repository, validating input against database state, handling errors and committing when everything goes well.
Rewrite the example that way and the logic moves into its own function, leaving the route a few lines long:
# services.py — one use case, called by every entry point
def sync_order(order_id, session, crm):
order = session.get(Order, order_id)
crm.push(order.to_dict())
order.synced = True
session.commit()
# app.py — the route only does web work
@app.route("/orders/<order_id>/sync", methods=["POST"])
def sync(order_id):
services.sync_order(order_id, db.session, crm_client)
return {"ok": True}
The nightly job loops over the orders and calls sync_order; the tests call it directly with a fake session and a fake CRM. The logic lives in one place, and whoever inherits the code only needs to read that place.
First thing to do at the client: before writing a second route, pull the first route’s logic out into a use case.
Idea two: Unit of Work groups operations into one block
Chapter 6 introduces Unit of Work, the book’s way of packaging the idea of atomic operations: grouping operations that must succeed or fail together into one block. Nothing is saved unless you explicitly call commit. Thanks to Python’s context managers, uncommitted work rolls back automatically when the request ends.
But look again at sync_order: the CRM receives the data before the commit. If the commit fails, the CRM has the order while the database still records it as unsynced, because a rollback can only undo what is in the database, not a call that has already gone out.
Reversing the order, committing first and then calling the CRM, does not save you either. AWS Prescriptive Guidance warns that if the service fails right after committing the transaction, the notification to the other system may never be sent.
Confluent calls this the dual-write problem: the database update succeeds but the external write fails, leaving the system in an inconsistent state.
Idea three: outbound calls must go through an outbox
The fix that both AWS and Confluent describe is the transactional outbox. You record the intent to call the CRM in an outbox table within the same transaction as the order, then let a separate worker make the call, retry on failure and mark it done once it has been sent successfully. Placed inside the book’s Unit of Work, sync_order becomes:
# services.py — after: the order and the outbox form one block
def sync_order(order_id, uow):
with uow:
order = uow.orders.get(order_id)
order.sync_status = "pending"
uow.outbox.add(event="order_sync", order_id=order_id,
idempotency_key=f"order-{order_id}")
uow.commit() # both are saved, or nothing is
# worker.py — runs separately, calls the CRM and retries
def flush_outbox(uow, crm):
with uow:
for msg in uow.outbox.pending():
order = uow.orders.get(msg.order_id)
crm.push(order.to_dict(), idempotency_key=msg.idempotency_key)
order.sync_status = "synced"
msg.mark_done()
uow.commit()
The order and the outbox record commit together or roll back together, so an order can no longer be saved while the intent to call the CRM disappears. If the CRM fails, the message stays in the outbox for the worker’s next run to retry.
The price is that messages are delivered at least once, which means the CRM may receive duplicates. AWS advises making the receiver idempotent by tracking messages already processed, and that is the job of idempotency_key. If the client’s API does not accept a key, the worker has to check for itself before resending.
A small exercise to test yourself: in flush_outbox, suppose crm.push succeeds but the worker crashes just before uow.commit(). What state are the order and the message in, and what will the next run do? Answer it yourself before reading on.
Answer: because nothing was committed, the Unit of Work rolls back, the message is still pending and the order is still unsynced. On the next run, the worker resends with the same idempotency_key, and that key is what stops the CRM from creating a second order. If you got both halves right, you understand why outbox and idempotency must always go together.
First thing to do at the client: list every place where code both writes to the database and calls an external system, then ask what happens if the second step fails.
A common mistake: adding layers before you feel the pain
After reading a patterns book, the biggest temptation is to build every layer from the first endpoint. For an FDE, each added layer is one more thing the client’s team must understand after you leave.
A simple commit function is still enough if the integration only touches the database. Add a layer when there is a specific pain, such as logic being copied into a second job or an outbound call that could be lost, not because the book has a chapter on it.
How to read it without drowning
Giuliano Pertile, in a 2022 review, still recommended the book but warned that its exercises for the reader are quite hard. The warning is fair, and it is also the reason to read it alongside a real project.
A sensible order: start with the Introduction to understand why code drifts into mud, move to the service layer chapter, then Unit of Work. Save the rest for when a project actually needs it.
Professionally, this is an easy skill to demonstrate. A personal repo with thin routes, a clear service layer, an outbox with idempotency keys and a test suite that runs without a real database says more than a line reading “clean architecture” on a CV. In an FDE interview, if you are asked about an integration you have built, explain how you handled an external system failing midway.
Most clients will never read your code. But the engineer who inherits it six months later will, and this book is written for exactly that person.
Was this article useful?
Thanks for the feedback!
8 sources
- Cosmic Python
- Preface – Architecture Patterns with Python
- Introduction – Architecture Patterns with Python
- Unit of Work Pattern
- Our First Use Case: Flask API and Service Layer
- Transactional outbox pattern – AWS Prescriptive Guidance
- Dual write problem – Confluent
- Book Review: Architecture Patterns with Python · 2022-06-01