Skip to content

Commit 9c19372

Browse files
docs: add the Domain-Driven Design article with its lab (#22)
A co-written, impersonal longread based on an internal talk, in English and Russian, with a lab that runs the article's own code blocks (domain in memory; Unit of Work, outbox and the email race on PostgreSQL 17.11 with sqlalchemy-foundation-kit 0.5.1, omni-box 0.3.1, SQLAlchemy 2.1.6). Site changes: hand-drawn inline SVG diagrams, a byline for co-written articles, the Architecture & DDD topic, a hand-drawn hero, a table of contents that follows the article, and parallel link validation in the build.
1 parent 0221025 commit 9c19372

33 files changed

Lines changed: 2782 additions & 21 deletions
17.7 KB
Loading
18.4 KB
Loading

‎docs/blog/.authors.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@ authors:
33
name: Alex Shalaev
44
description: Author & Maintainer
55
avatar: https://github.com/bedrock-python.png
6+
stas:
7+
name: Stanislav Shimovolos
8+
description: Co-author
9+
avatar: https://github.com/stanislaushimovolos.png
10+
url: https://github.com/stanislaushimovolos

‎docs/blog/archive/index.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Every post in the Bedrock Python blog, grouped by year.
44

55
## 2026
66

7+
- **2026-11-02** — [Domain-Driven Design: the model first, the patterns later](../posts/2026-11-02-domain-driven-design.md)
78
- **2026-10-29** — [Why AI review findings need verification](../posts/2026-10-29-ai-code-review-should-not-be-fully-autonomous.md)
89
- **2026-10-27** — [Why extract Python service infrastructure into libraries](../posts/2026-10-27-why-bedrock-python-libraries.md)
910
- **2026-10-26** — [Documentation that lets an AI coding agent write checkable code](../posts/2026-10-26-documentation-for-ai-coding-agents.md)

‎docs/blog/catalog.json‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
11
{
22
"articles": [
3+
{
4+
"id": "2026-11-02-domain-driven-design",
5+
"title": "Domain-Driven Design: the model first, the patterns later",
6+
"description": "Every serious service has rules that developers did not invent: when a customer gets a discount, who counts as an employee, how much cargo a ship may take over its limit.",
7+
"url": "posts/2026-11-02-domain-driven-design/",
8+
"date": "2026-11-02",
9+
"categories": [
10+
"design"
11+
],
12+
"tags": [
13+
"ddd",
14+
"domain-driven-design",
15+
"architecture",
16+
"testing",
17+
"sqlalchemy-foundation-kit",
18+
"omni-box"
19+
],
20+
"topics": [
21+
"architecture",
22+
"postgres",
23+
"messaging"
24+
],
25+
"minutes": 37,
26+
"search": "Domain-Driven Design: the model first, the patterns later Every serious service has rules that developers did not invent: when a customer gets a discount, who counts as an employee, how much cargo a ship may take over its limit. ddd domain-driven-design architecture testing sqlalchemy-foundation-kit omni-box Design Architecture & DDD PostgreSQL & SQLAlchemy Kafka & messaging The model matters more than the code Ubiquitous language Bounded Context: where a model ends Core, supporting, generic: where to spend effort Dependencies point at the core Rich model and anemic model Step one: types instead of primitives Step two: behavior moves into the entity Make the implicit explicit Functional core, imperative shell The trilemma: where to check that an email is free Option 1: check in the controller Option 2: a repository in the domain Option 3: load everything up front Two out of three CanExecute / Execute What if the requests arrive at the same time Tactical patterns Value Object Entity Aggregate Repositories and factories Does a repository need an interface Reads that bypass aggregates Unit of Work Domain Events Domain layer and application layer Specification DDD and tests What to mock Public API When you do not need DDD Further reading"
27+
},
328
{
429
"id": "2026-10-29-ai-code-review-should-not-be-fully-autonomous",
530
"title": "Why AI review findings need verification",
@@ -436,6 +461,10 @@
436461
{
437462
"id": "engineering",
438463
"label": "Python & tooling"
464+
},
465+
{
466+
"id": "architecture",
467+
"label": "Architecture & DDD"
439468
}
440469
],
441470
"categories": [

‎docs/blog/category/design.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
Architecture and decisions behind the ecosystem.
44

5+
- **2026-11-02** — [Domain-Driven Design: the model first, the patterns later](../posts/2026-11-02-domain-driven-design.md)
56
- **2026-10-21** — [A core without dependencies: install the integrations your service needs](../posts/2026-10-21-zero-dependency-cores.md)
67
- **2026-10-18** — [PostgreSQL partition maintenance: from a plan to a verified archive](../posts/2026-10-18-postgresql-partition-maintenance.md)
78
- **2026-10-08** — [SQLAlchemy sessions and transactions: who owns commit](../posts/2026-10-08-sqlalchemy-sessions-and-transactions.md)

‎docs/blog/category/index.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Browse posts by topic.
55
<nav class="bdr-category-grid" aria-label="Blog categories" markdown="0">
66
<a class="bdr-category-card" href="libraries/"><span><strong>Libraries</strong><small>0 articles</small></span><p>Design notes and deep dives into the Python packages.</p><span class="bdr-category-card__arrow" aria-hidden="true">↗</span></a>
77
<a class="bdr-category-card" href="tools/"><span><strong>Tools</strong><small>1 article</small></span><p>Developer tools, code review and workspace search.</p><span class="bdr-category-card__arrow" aria-hidden="true">↗</span></a>
8-
<a class="bdr-category-card" href="design/"><span><strong>Design</strong><small>11 articles</small></span><p>Architecture and decisions behind the ecosystem.</p><span class="bdr-category-card__arrow" aria-hidden="true">↗</span></a>
8+
<a class="bdr-category-card" href="design/"><span><strong>Design</strong><small>12 articles</small></span><p>Architecture and decisions behind the ecosystem.</p><span class="bdr-category-card__arrow" aria-hidden="true">↗</span></a>
99
<a class="bdr-category-card" href="tutorials/"><span><strong>Tutorials</strong><small>5 articles</small></span><p>Practical guides to using the libraries in your services.</p><span class="bdr-category-card__arrow" aria-hidden="true">↗</span></a>
1010
<a class="bdr-category-card" href="meta/"><span><strong>Meta</strong><small>2 articles</small></span><p>Announcements about the blog and ecosystem.</p><span class="bdr-category-card__arrow" aria-hidden="true">↗</span></a>
1111
</nav>

‎docs/blog/index.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ hide:
6161
<button data-blog-topic="messaging" type="button" aria-pressed="false"><span class="bdr-topic-symbol" aria-hidden="true">⇢</span><span>Kafka & messaging</span><span data-topic-count></span></button>
6262
<button data-blog-topic="lifecycle" type="button" aria-pressed="false"><span class="bdr-topic-symbol" aria-hidden="true">◷</span><span>Service lifecycle</span><span data-topic-count></span></button>
6363
<button data-blog-topic="redis" type="button" aria-pressed="false"><span class="bdr-topic-symbol" aria-hidden="true">◇</span><span>Redis & idempotency</span><span data-topic-count></span></button>
64+
<button data-blog-topic="architecture" type="button" aria-pressed="false"><span class="bdr-topic-symbol" aria-hidden="true">▣</span><span>Architecture & DDD</span><span data-topic-count></span></button>
6465
<button data-blog-topic="engineering" type="button" aria-pressed="false"><span class="bdr-topic-symbol" aria-hidden="true">⌘</span><span>Python & tooling</span><span data-topic-count></span></button>
6566
</nav>
6667
</details>
@@ -85,6 +86,18 @@ hide:
8586
<div class="bdr-active-filters" data-blog-active hidden></div>
8687
<div class="bdr-results" id="blog-results" data-blog-results>
8788
<!-- catalog:articles:start -->
89+
<article class="bdr-entry" data-article-id="2026-11-02-domain-driven-design">
90+
<a class="bdr-entry__link bdr-card" href="posts/2026-11-02-domain-driven-design/">
91+
<div class="bdr-entry__body">
92+
<div class="bdr-entry__eyebrow">Architecture &amp; DDD <span>·</span> Design</div>
93+
<h3 class="bdr-entry__title">Domain-Driven Design: the model first, the patterns later</h3>
94+
<p class="bdr-entry__description">Every serious service has rules that developers did not invent: when a customer gets a discount, who counts as an employee, how much cargo a ship may take over its limit.</p>
95+
<div class="bdr-entry__meta"><time datetime="2026-11-02">Nov 2, 2026</time><span>·</span><span>37 min read</span></div>
96+
</div>
97+
<div class="bdr-entry__visual bdr-card__visual" aria-hidden="true"></div>
98+
<span class="bdr-entry__arrow" aria-hidden="true">↗</span>
99+
</a>
100+
</article>
88101
<article class="bdr-entry" data-article-id="2026-10-29-ai-code-review-should-not-be-fully-autonomous">
89102
<a class="bdr-entry__link bdr-card" href="posts/2026-10-29-ai-code-review-should-not-be-fully-autonomous/">
90103
<div class="bdr-entry__body">
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Lab: the code from the Domain-Driven Design article
2+
3+
Runnable checks for [the article](../../posts/2026-11-02-domain-driven-design.md).
4+
Docker must be running. The script creates and removes its own PostgreSQL container;
5+
it does not use an existing database. Run from this directory in a repository checkout:
6+
7+
```bash
8+
uv run --no-project --python 3.13 --with-requirements requirements.txt ddd_lab.py
9+
```
10+
11+
Dependencies are pinned in `requirements.txt`: sqlalchemy-foundation-kit 0.5.1, omni-box 0.3.1,
12+
SQLAlchemy 2.1.6, asyncpg 0.32.0 and testcontainers 4.15.0. The image is `postgres:17-alpine`;
13+
the script prints its server version (17.11 in the verified run).
14+
sqlalchemy-foundation-kit 0.5.1 is the first release that imports on SQLAlchemy 2.1
15+
([#78](https://github.com/bedrock-python/sqlalchemy-foundation-kit/issues/78), found while writing this lab).
16+
17+
The lab keeps no copy of the article's code. `ddd_lab.py` reads the code blocks from the English
18+
and Russian editions, checks that both show the same code, and runs each block in a fresh namespace
19+
with the names it takes for granted. `support.py` holds what the article leaves out: the tables,
20+
the repositories and the full `User` with CanExecute / Execute and domain events.
21+
22+
In memory it checks that:
23+
24+
- the anemic `PersonService` leaves `is_employee` stale, as the article says;
25+
- the eleventh order makes a `Customer` Advanced, and the orders are read-only;
26+
- `OverbookingPolicy` accepts 110% of capacity and refuses 111%;
27+
- the functional core returns a `FileUpdate` that the shell applies, and the article's test passes without a disk;
28+
- each of the three trilemma options rejects a taken address;
29+
- with CanExecute / Execute the controller asks first, and calling `change_email` around the check raises (the block has no `assert`);
30+
- `Money` is equal by value and checks currency and sign; `Order` is equal by id and stays hashable;
31+
- `transfer`, `DelinquentInvoice` and the `MessageBus` wrapper behave as described.
32+
33+
On PostgreSQL it adds the article's unique constraint with its `ALTER TABLE` and runs the assembled `change_email`:
34+
35+
- the user, the company and the outbox row commit together, and the event carries `aggregate_type="user"`;
36+
- a confirmed email and a taken address leave nothing written;
37+
- a failing outbox write rolls back the user and the company;
38+
- two transactions both read the address as free before either writes; the constraint lets one commit, and the other gets `Email is taken`;
39+
- with ORM-tracked saves, the violation surfaces at the flush that `transaction()` runs before commit, still as `IntegrityError`.
40+
41+
Fragments the article prints as partial classes (the aggregate, the repository over `_db`, the `User` that records events)
42+
are not run as printed; `support.py` has the complete versions the PostgreSQL checks use.
43+
The script prints `PASS: article snippets` only when all assertions succeed.

0 commit comments

Comments
 (0)