<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>Fabian Braun</title>
  <subtitle>Systems for making better decisions — in investing, software, and content architecture.</subtitle>
  <link href="https://www.fabian-braun.de/feed.xml" rel="self" type="application/atom+xml"/>
  <link href="https://www.fabian-braun.de/" rel="alternate" type="text/html"/>
  <id>https://www.fabian-braun.de/</id>
  <updated>2026-10-13T08:53:45+02:00</updated>
  <author>
    <name>Fabian Braun</name>
    <uri>https://www.fabian-braun.de/</uri>
  </author>
  
  
  <entry>
    <title>Translating a page is solved. Keeping it translated isn&apos;t.</title>
    
    <link href="https://www.fabian-braun.de/writing/keeping-it-translated/" rel="alternate" type="text/html"/>
    
    <id>https://www.fabian-braun.de/writing/keeping-it-translated/</id>
    <published>2026-10-10T00:00:00+02:00</published>
    <updated>2026-10-10T00:00:00+02:00</updated>
    <category term="content-systems"/>
    <summary>Machine translation made the first translation cheap. The hard part is every day after it: knowing what is stale, what is inconsistent, and what nobody approved.</summary>
    
    <content type="html" xml:base="https://www.fabian-braun.de/writing/keeping-it-translated/">&lt;p&gt;In 2026, translating a page costs one click and a few cents.&lt;/p&gt;

&lt;p&gt;Almost every CMS has a DeepL integration or an AI plugin by now, and every LLM will translate
whatever you paste into it, in any tone you ask for. If your problem is “I have an English page
and I need a German one”, that problem is solved, and solved well.&lt;/p&gt;

&lt;p&gt;So why do multilingual sites still feel like a chore? Why does every agency I talk to have a
story about the French version that was “a bit behind” for half a year?&lt;/p&gt;

&lt;p&gt;Because the hard part was never the first translation. It’s every day after it.&lt;/p&gt;

&lt;h2 id=&quot;the-day-after-launch&quot;&gt;The day after launch&lt;/h2&gt;

&lt;p&gt;Here’s how it usually goes. The site launches in English, German and French. Everything is
translated, reviewed, published. Everyone is happy.&lt;/p&gt;

&lt;p&gt;Three weeks later, marketing changes the pricing paragraph on the English page. Someone
updates the German page, because the German market matters this quarter. Nobody touches
the French one. Nothing tells anyone that it is now wrong.&lt;/p&gt;

&lt;p&gt;A month later, the blog gets a new post. The blog is a separate module, translated with a
different tool, by a different person, and the product is called “Starter plan” on the pages
and “Basic package” in the blog.&lt;/p&gt;

&lt;p&gt;Half a year later, someone adds Polish. Three hundred pages, a blog, a product catalog. The
translation button works on each page. It doesn’t tell you which three hundred pages, or what
it will cost, or which of them were going to change next week anyway.&lt;/p&gt;

&lt;p&gt;None of this is a translation problem. Every single sentence was translated perfectly. It’s a
&lt;strong&gt;state&lt;/strong&gt; problem: nobody knows what is current, what is behind, and what is inconsistent.&lt;/p&gt;

&lt;h2 id=&quot;its-not-a-made-up-story&quot;&gt;It’s not a made-up story&lt;/h2&gt;

&lt;p&gt;That story is invented, but the problems aren’t. Read the public issue trackers of open-source
CMSs and their translation plugins, and the same complaints come up again and again, from
agencies, in-house teams and single developers. All quotes below are real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Which pages are out of sync?”&lt;/strong&gt; An agency asked for a table with “the languages” as columns
and “the status of translation” for every page, because “once you lost track it nearly
impossible to track it down if you have many pages”. A team with tens of thousands of
machine-translated pages wrote that they “need to see the number of Pages that have manual
translations”, and that it took SQL queries against the database to find out. Others just want
the editing screen to show which language tabs are still empty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“I changed a comma and the translation is gone.”&lt;/strong&gt; One team reported that changing the
source “just deletes the previous translation without fallback … even if it’s just a small
change like a comma missing”. Another put it more bluntly: “translated strings are repeatedly
lost when a user changes the source string. This makes regular users REALLY angry!”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Half-translated pages go live.”&lt;/strong&gt; A developer found that the only way to apply translations
to a page was to publish it: a page that’s halfway translated can’t be saved as a draft.
Another, adding Spanish to an English site, found that “all the Spanish pages will be instantly
published, and will use the English content”.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“We wrote our own script.”&lt;/strong&gt; One developer was trying “to automate the translation of a …
website with hundreds of pages” and set out to write a script that sends everything to DeepL.
Others ask for empty fields to be filled by a local LLM in the background. Every team builds,
or wishes for, the same plumbing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“And what about everything that isn’t a page?”&lt;/strong&gt; Products, reusable content blocks and other
data outside the page tree don’t fit the page-based workflow, an agency explained; others asked
for a way to filter that content by translation status at all.&lt;/p&gt;

&lt;p&gt;One agency summed it up: “The top customer complaint we get is probably that the sync does not
work.”&lt;/p&gt;

&lt;h2 id=&quot;four-things-a-translate-button-cant-know&quot;&gt;Four things a translate button can’t know&lt;/h2&gt;

&lt;p&gt;When I list what goes wrong after the first translation, it falls into four groups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Currency: which translations are out of date?&lt;/strong&gt;
To know that the French pricing paragraph is stale, something has to remember &lt;em&gt;which version
of the English text&lt;/em&gt; the French one was made from. A button that translates on demand has no
such memory. It translates; it forgets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Consistency: are we saying the same thing everywhere?&lt;/strong&gt;
Product names, legal terms, the formal or informal “you” (&lt;em&gt;Sie&lt;/em&gt; or &lt;em&gt;du&lt;/em&gt;, &lt;em&gt;vous&lt;/em&gt; or &lt;em&gt;tu&lt;/em&gt;). Each
translation is reasonable on its own; together they contradict each other. Consistency needs a
glossary and a memory of how we translated things before, shared across every place where
content lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Control: who decided this is ready?&lt;/strong&gt;
Machine translation is good enough to be dangerous. It reads fluently, so nobody checks it.
A broken link, a mangled placeholder, an alt text that says nothing, a brand name translated
into a common noun. The fix isn’t to avoid machine translation; it’s to make sure nothing is
published without a person approving it, and that obvious errors are caught before that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Coverage: is &lt;em&gt;all&lt;/em&gt; the content in the picture?&lt;/strong&gt;
Pages live in the CMS. The blog is a separate module. Products live in a shop or a catalog
database. Most tools see one of these. The editor sees all of them, and so does the visitor.&lt;/p&gt;

&lt;p&gt;What these four have in common: they need &lt;strong&gt;memory over time&lt;/strong&gt;. Which source text was each
translation based on? Which terms did we agree on? What did a person approve, and when? Which
content exists in which languages?&lt;/p&gt;

&lt;p&gt;A plugin that runs when you click a button is stateless by design, and that’s fine for what
it does. But the work that eats agencies’ time is stateful.&lt;/p&gt;

&lt;h2 id=&quot;what-id-want-as-an-editor&quot;&gt;What I’d want as an editor&lt;/h2&gt;

&lt;p&gt;Strip away the technology, and what an editor needs is surprisingly simple:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;A list of what’s behind&lt;/strong&gt;, per language: “French: 12 texts out of date, 3 missing.” Not a
feeling, a list.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;One action to fix it&lt;/strong&gt;: retranslate exactly those texts, nothing else, and put the
results somewhere I can review them. A draft, not a published page.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The same words everywhere&lt;/strong&gt;: if I decide that “Starter plan” is “Einstiegstarif” in German,
that decision applies to the pages, the blog and the product catalog.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Errors caught before I see them&lt;/strong&gt;: a link that disappeared, a placeholder that broke, a
meta description that’s now 240 characters long.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;For agencies: the same overview across every client site&lt;/strong&gt;, so the question “which client
is behind?” has an answer before the client asks it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is exotic. It’s how software teams have handled translations of interface texts
for decades: translation files know when a source string changed and mark the translation as
&lt;em&gt;fuzzy&lt;/em&gt;, to be checked, instead of throwing it away. Content never got the same treatment,
because content lives in many places, many formats and many tools.&lt;/p&gt;

&lt;h2 id=&quot;what-makes-it-hard&quot;&gt;What makes it hard&lt;/h2&gt;

&lt;p&gt;If it’s so obvious, why doesn’t every CMS do it?&lt;/p&gt;

&lt;p&gt;Because “which version was this translated from?” is harder than it sounds once content is
structured. A page is a tree of components or blocks, often with new IDs every time a new
version is saved. One system stores translations as extra rows, the next as extra columns, a
third as JSON in a single field. To say “this paragraph in French is stale”, you need a stable
identity for every translatable piece across versions, languages and storage formats, and a
reliable way to tell whether its source changed.&lt;/p&gt;

&lt;p&gt;That’s plumbing. It’s not glamorous, and no single plugin has a reason to build it across all
the places content lives.&lt;/p&gt;

&lt;h2 id=&quot;what-im-building&quot;&gt;What I’m building&lt;/h2&gt;

&lt;p&gt;Disclosure: I’m the django CMS fellow, and what follows is my own project, Ponyglot, not a
django CMS Association product.&lt;/p&gt;

&lt;p&gt;Ponyglot is that plumbing, for sites built on Django: open-source connectors for django CMS,
Wagtail, django-parler and django-modeltranslation extract every translatable field into one
model, where each piece has a stable key and a fingerprint of its source text. Taking the
problems above one by one:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;“Which pages are out of sync?”&lt;/strong&gt; A matrix per page of what’s up to date, stale, in review or
missing, per language; a stale overview per site; and the same counts across all your sites.
Content outside the page tree gets the same status as pages.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;“I changed a comma.”&lt;/strong&gt; The text shows as &lt;em&gt;stale&lt;/em&gt;, like a fuzzy entry in a translation file.
The live translation stays until a new one is approved, and translations an editor wrote by
hand are never overwritten.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;“Half-translated pages go live.”&lt;/strong&gt; Results arrive as drafts, or as suggestions with a diff
for content that has no drafts. Publishing is the approval; nothing is published
automatically.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;“We wrote our own script.”&lt;/strong&gt; “Translate stale and missing” is one action, with a cost
estimate first, from the dashboard, the CMS admin or the command line.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;“Everything that isn’t a page.”&lt;/strong&gt; Pages, blog, products and other content share one
glossary, one translation memory and one status.&lt;/li&gt;
  &lt;li&gt;And QA checks for links, placeholders, HTML, length and SEO flag errors before anything is
published.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It doesn’t solve everything. Preparing a change as a draft in the source &lt;em&gt;and&lt;/em&gt; the
translations at the same time, one of the most-requested wishes, is still open: Ponyglot
translates the published source. Regional variants derived from a parent language (French for
Belgium from French for France) aren’t modelled yet either.&lt;/p&gt;

&lt;p&gt;The translation itself is the boring part: DeepL or an EU-hosted LLM, whichever fits the
language. The free tools are great at it, and I’d rather work with them than against them.&lt;/p&gt;

&lt;p&gt;We’re onboarding a small group of agencies and product teams now. If your multilingual site
has a French version that’s “a bit behind”, I’d love to hear how you handle it today:
&lt;a href=&quot;https://ponyglot.app/?ref=blog-keeping-translated&quot;&gt;ponyglot.app&lt;/a&gt;.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;Next in this series: how to know which translations are stale, with fingerprints and a few
lines of code.&lt;/em&gt;&lt;/p&gt;
</content>
    
  </entry>
  
  
  <entry>
    <title>AI Doesn&apos;t Replace Engineering. It Moves It Up the Stack.</title>
    
    <link href="https://www.fabian-braun.de/writing/ai-moves-engineering-up-the-stack/" rel="alternate" type="text/html"/>
    
    <id>https://www.fabian-braun.de/writing/ai-moves-engineering-up-the-stack/</id>
    <published>2026-10-07T00:00:00+02:00</published>
    <updated>2026-10-07T00:00:00+02:00</updated>
    <category term="automation-ai"/>
    <summary>Every layer we automate makes the layer above it the new bottleneck. AI is another step in an old progression — and it changes the size of the thing an engineer can hold.</summary>
    
    <content type="html" xml:base="https://www.fabian-braun.de/writing/ai-moves-engineering-up-the-stack/">&lt;p&gt;In &lt;a href=&quot;/writing/open-source-is-dying-again/&quot;&gt;Open Source Is Dying Again&lt;/a&gt;, I argued that when code becomes abundant, architecture counts. That was a claim about &lt;em&gt;what&lt;/em&gt; becomes scarce. This one is about the mechanism behind it, and where it leads if you keep following it.&lt;/p&gt;

&lt;p&gt;The usual debate is whether AI can replace engineers. I think that’s the wrong question. “It can’t, because it makes mistakes” ages badly with every model release. “It can, look at this demo” ignores everything that happens after the demo. The more useful observation is this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every layer we successfully automate makes the layer above it the new bottleneck.&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;following-the-bottleneck&quot;&gt;Following the bottleneck&lt;/h2&gt;

&lt;p&gt;Not long ago, the expensive step was writing the implementation. AI tools now take a large part of it, and agentic workflows extend the chain until routine maintenance runs largely on its own:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;problem → implementation → tests → documentation
issue → diagnosis → implementation → tests → pull request
dependency update → compatibility analysis → fixes → CI → release candidate
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Excellent. We should automate all of it. There is no reason to keep repetitive mechanical work manual just for its own sake; the version pins will not miss us.&lt;/p&gt;

&lt;p&gt;Once the pull request appears by itself, the question that takes the time is no longer &lt;em&gt;how do I write this?&lt;/em&gt; but &lt;em&gt;should we merge it?&lt;/em&gt; So we automate much of the review as well — architectural analysis, compatibility risks, a suggested decision — and the bottleneck moves again:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Should we merge it?
  → Does this belong in the product?
    → What should the product become?
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Each step is real progress. Each also moves the remaining work somewhere harder to describe, harder to measure, and harder to hand over.&lt;/p&gt;

&lt;h2 id=&quot;an-issue-that-fixes-itself&quot;&gt;An issue that fixes itself&lt;/h2&gt;

&lt;p&gt;Take a mature library and an ordinary issue:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Feature X doesn’t work on Python 3.15.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An agent can plausibly handle all of it. It reproduces the failure, finds the change in Python that caused it, adapts the implementation, adds a regression test, runs the compatibility matrix, writes a changelog entry, and opens a pull request.&lt;/p&gt;

&lt;p&gt;Measured by the activities we used to call engineering, almost nothing is left to do.&lt;/p&gt;

&lt;p&gt;Yet someone still has to decide whether Python 3.15 should be supported yet. Whether the fix preserves compatibility for every user, not only for the test suite. Whether the changed abstraction is the right one, or merely the one that made the tests pass. Whether this warrants a release, and whether downstream projects need to know before it ships.&lt;/p&gt;

&lt;p&gt;The code alone doesn’t determine any of these answers. They also depend on the project’s history, its users, its promises, and where it is going. Much of the engineering in this example is genuinely gone. What remains has moved up.&lt;/p&gt;

&lt;h2 id=&quot;we-have-done-this-before&quot;&gt;We have done this before&lt;/h2&gt;

&lt;p&gt;That pattern is not new. Every major advance in software engineering has removed work developers at the time considered essential. Compilers removed most assembly programming. Garbage collection removed manual memory management from most application code. Frameworks removed enormous amounts of plumbing. Cloud platforms and managed databases removed much of the work of running servers and keeping them alive.&lt;/p&gt;

&lt;p&gt;Much of that difficulty didn’t move anywhere. It disappeared. Nobody mourns that web developers no longer allocate registers or implement TCP, and nobody had to pick that work up elsewhere.&lt;/p&gt;

&lt;p&gt;But none of these steps eliminated engineering. Developers stopped worrying about memory layout and started worrying about data models, latency, consistency and user experience — problems that were always there, but out of reach behind the work of getting anything to run at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some difficulty disappears entirely. What remains moves upward.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI is another step in that progression, unusual mainly in its size and speed. It can genuinely eliminate large amounts of engineering work. The question is what remains, and where it goes.&lt;/p&gt;

&lt;h2 id=&quot;from-code-to-intent&quot;&gt;From code to intent&lt;/h2&gt;

&lt;p&gt;The question used to be: &lt;em&gt;how do I implement this?&lt;/em&gt; Increasingly it is: &lt;em&gt;what exactly should happen?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I rarely write tests from scratch anymore. I still decide what needs testing, which behaviour is a contract, and whether a test proves what it claims to prove. Typing the fixtures and assertions is increasingly a machine’s job. Oddly, that hasn’t made me think less about testing — only at a different level.&lt;/p&gt;

&lt;p&gt;A vague ticket used to be partly self-correcting: a developer would run into the awkward case and come back with a question. Implementation was slow enough to double as discovery. An agent working from the same ticket produces, quickly and confidently, a beautifully implemented version of the wrong thing.&lt;/p&gt;

&lt;p&gt;So more of the work goes into expressing &lt;strong&gt;intent&lt;/strong&gt; precisely. That isn’t prompt engineering. It is the part of engineering we used to do implicitly, while typing.&lt;/p&gt;

&lt;h2 id=&quot;from-implementation-to-boundaries&quot;&gt;From implementation to boundaries&lt;/h2&gt;

&lt;p&gt;When you can build almost anything, &lt;em&gt;what not to build&lt;/em&gt; matters more. Should this be in the core or an extension? Is this an API, or a detail that should stay free to change? Do these two things share an abstraction, or just look similar today?&lt;/p&gt;

&lt;p&gt;AI is very good at producing abstractions when asked. It is much less useful at telling you whether you should have one in the first place.&lt;/p&gt;

&lt;p&gt;Boundaries were always the expensive decisions: you can’t cheaply undo them once others depend on them. When the code inside them becomes cheap, the boundaries are most of what’s left.&lt;/p&gt;

&lt;h2 id=&quot;from-components-to-systems&quot;&gt;From components to systems&lt;/h2&gt;

&lt;p&gt;This is the change I find most interesting.&lt;/p&gt;

&lt;p&gt;Instead of spending an afternoon implementing one component, I can spend it exploring three approaches. An agent prototypes each one. Another generates the migrations, writes integration tests, and checks what breaks for existing users. I read the results, throw two away, and keep the parts of the third that worked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI doesn’t merely make the same engineer faster. It changes the size of the thing an engineer can hold and manipulate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not long ago, I mostly manipulated functions. Then modules. Increasingly, whole subsystems: I can ask what would happen if a layer worked differently and get an answer in the form of working code, not a diagram and a guess.&lt;/p&gt;

&lt;p&gt;That is a different job, not a faster version of the old one.&lt;/p&gt;

&lt;h2 id=&quot;architecture-becomes-executable&quot;&gt;Architecture becomes executable&lt;/h2&gt;

&lt;p&gt;Historically, architecture was expensive to test. You could argue for approach A or approach B, but building both properly might take months. So architectural decisions rested heavily on prediction: the most experienced person in the room imagined how each option would behave under load, under change, and under five years of feature requests.&lt;/p&gt;

&lt;p&gt;If prototypes become cheap enough, architecture becomes increasingly &lt;strong&gt;empirical&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of three meetings about whether an approach will work, have agents build representative slices of both. Benchmark them. Write the ugly cases. Simulate the migration from the current system. Try next year’s likely feature request and see which design absorbs it.&lt;/p&gt;

&lt;p&gt;That doesn’t make experience worthless. Experience is what tells you which slices are representative and which ugly cases matter. And it changes what the valuable skill is: when producing answers becomes cheap, knowing &lt;strong&gt;which uncertainty is worth spending computation on&lt;/strong&gt; becomes the scarce part. Seniority may come to mean less knowing the answers, and more knowing which questions are worth paying to answer.&lt;/p&gt;

&lt;h2 id=&quot;it-doesnt-stop-at-architecture&quot;&gt;It doesn’t stop at architecture&lt;/h2&gt;

&lt;p&gt;So far, this sounds like an argument that architects are safe. I don’t think the movement stops there.&lt;/p&gt;

&lt;p&gt;Imagine software maintenance that is close to autonomous. Agents watch dependencies, fix regressions, review each other’s changes, prepare releases and monitor production. Architectural analysis is excellent, and the suggested decisions are usually right.&lt;/p&gt;

&lt;p&gt;What is left is &lt;strong&gt;product judgment&lt;/strong&gt;. Which users matter most when their needs conflict? Which incompatibilities are worth imposing? Which architecture enables the next five years rather than the next release? Which feature request is a local requirement that should stay out of the core? When should an old abstraction finally be allowed to die?&lt;/p&gt;

&lt;p&gt;There is an old word for the people who make those calls on a long-lived system: stewards. A steward carries enough context to know why the system looks the way it does, enough authority to decide where it goes next, and enough continuity to live with the consequences. None of those three is a property of code, and none of them is produced by producing more of it.&lt;/p&gt;

&lt;p&gt;Now suppose AI becomes excellent at recommending product decisions too. It models the user base, simulates migration costs, and proposes a roadmap better argued than most human ones.&lt;/p&gt;

&lt;p&gt;What is left then is &lt;strong&gt;accountability&lt;/strong&gt;. Someone still has to decide whose interests the system serves, and answer for it when those interests conflict. Accountability isn’t primarily a capability. It is a role. It exists because the people affected by a decision need someone who can be asked, persuaded, challenged, overruled or replaced.&lt;/p&gt;

&lt;p&gt;Followed all the way through, the stack looks like this:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;code → intent → boundaries → architecture → product judgment → accountability
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Each layer was always there. What changes is how much of the work sits in it.&lt;/p&gt;

&lt;h2 id=&quot;the-uncomfortable-part&quot;&gt;The uncomfortable part&lt;/h2&gt;

&lt;p&gt;None of this means every engineer is fine.&lt;/p&gt;

&lt;p&gt;The risk isn’t that engineering disappears. It’s that engineers keep working at the old layer while that layer is automated underneath them.&lt;/p&gt;

&lt;p&gt;Someone who describes their job as&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;I turn tickets into Python.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;has a problem, because that description is now a reasonable specification for software.&lt;/p&gt;

&lt;p&gt;Someone who describes it as&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;I understand this domain well enough to turn ambiguous needs into systems that survive contact with reality.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;has suddenly acquired extraordinarily powerful tools.&lt;/p&gt;

&lt;p&gt;There is a harder question hidden here. Nobody is born with context. People learned to work at the higher layers by spending years at the lower ones: fixing bugs in unfamiliar corners of a codebase, writing tests that reveal how a system really behaves, implementing small features and watching them collide with large ones. That work was never only output. It was also how people learned what the output meant.&lt;/p&gt;

&lt;p&gt;There is a paradox here: AI may make experienced engineers dramatically more capable while making it harder for inexperienced engineers to become experienced.&lt;/p&gt;

&lt;p&gt;If agents now do that work better, AI doesn’t only move engineering upward. &lt;strong&gt;It may remove the ladder people used to climb to get there.&lt;/strong&gt; Learning will have to happen more deliberately — closer to the people who already carry the context, as programs like &lt;a href=&quot;https://djangonaut.space&quot;&gt;Djangonaut Space&lt;/a&gt; already practise. That is a real problem. But it is a problem of how we pass on the craft, not a sign that the craft is going away.&lt;/p&gt;

&lt;h2 id=&quot;code-was-never-the-decision&quot;&gt;Code was never the decision&lt;/h2&gt;

&lt;p&gt;We have been moving up the stack for as long as software engineering has existed.&lt;/p&gt;

&lt;p&gt;AI is unsettling because the layer being automated this time is the one many of us identify as engineering itself: writing code. For a generation of developers, the code &lt;em&gt;was&lt;/em&gt; the work, and being good at it was being good at the job.&lt;/p&gt;

&lt;p&gt;I suspect that identification will come to look as strange as equating programming with register allocation.&lt;/p&gt;

&lt;p&gt;Code is one representation of an engineering decision. It has never been the decision itself.&lt;/p&gt;

&lt;p&gt;If machines become extraordinarily good at producing the representation, we get to spend more of our time on what mattered all along: understanding what should exist, how its parts should fit together, and who answers for the choices we make.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI doesn’t replace engineering. It moves it up the stack.&lt;/strong&gt;&lt;/p&gt;
</content>
    
  </entry>
  
  
  <entry>
    <title>One Operation, Three Interfaces: A Service Architecture for django CMS</title>
    
    <link href="https://www.django-cms.org/resources/blog/2026/10/05/one-operation-three-interfaces-a-service-architecture-for-django-cms/" rel="alternate" type="text/html"/>
    
    <id>https://www.fabian-braun.de/writing/one-operation-three-interfaces/</id>
    <published>2026-10-05T00:00:00+02:00</published>
    <updated>2026-10-05T00:00:00+02:00</updated>
    <category term="content-systems"/>
    <summary>How shared content operations could support editors, REST integrations, and AI tools.</summary>
    
    <content type="html">&lt;p&gt;How shared content operations could support editors, REST integrations, and AI tools.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.django-cms.org/resources/blog/2026/10/05/one-operation-three-interfaces-a-service-architecture-for-django-cms/&quot;&gt;Read on django-cms.org ↗&lt;/a&gt;&lt;/p&gt;</content>
    
  </entry>
  
  
  <entry>
    <title>Receiving and Parsing XRechnung Invoices in Django</title>
    
    <link href="https://www.fabian-braun.de/writing/receiving-and-parsing-xrechnung-invoices-in-django/" rel="alternate" type="text/html"/>
    
    <id>https://www.fabian-braun.de/writing/receiving-and-parsing-xrechnung-invoices-in-django/</id>
    <published>2026-10-03T00:00:00+02:00</published>
    <updated>2026-10-03T00:00:00+02:00</updated>
    <category term="software-reuse"/>
    <summary>What building the inbound side of django-xrechnung taught me about treating invoices as untrusted input, as evidence, and as data — and why &quot;valid&quot; is the least interesting thing an e-invoice can be.</summary>
    
    <content type="html" xml:base="https://www.fabian-braun.de/writing/receiving-and-parsing-xrechnung-invoices-in-django/">&lt;p&gt;Since January 2025, every business in Germany must be able to receive electronic invoices. Not PDFs attached to an email, but structured invoices following the European standard EN 16931: XRechnung in its UBL or CII syntax, or a hybrid format such as ZUGFeRD/Factur-X, where the XML travels inside a PDF. The obligation to &lt;em&gt;send&lt;/em&gt; them phases in during 2027 and 2028. The obligation to &lt;em&gt;accept&lt;/em&gt; them is already here.&lt;/p&gt;

&lt;p&gt;Most of the discussion so far has been about sending. That makes sense: issuing an invoice is something you control. You choose the format, you generate the XML, you run the validator, and if it fails you fix your own data.&lt;/p&gt;

&lt;p&gt;Receiving is a different problem. You get whatever your suppliers’ software produces. Some of it is excellent. Some of it is a PDF with an XML attached that disagrees with the PDF. Some of it isn’t an invoice at all.&lt;/p&gt;

&lt;p&gt;I have been building &lt;a href=&quot;https://codeberg.org/fsbraun/django-xrechnung&quot;&gt;django-xrechnung&lt;/a&gt;, a reusable Django application for issuing and receiving XRechnung documents. The outgoing side took most of the code. The incoming side took most of the thinking. This article is about that thinking — for Django developers who will build something similar, and for the people who will have to decide what their business does with the invoices that arrive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An incoming e-invoice is three things at once: untrusted input, legal evidence, and business data. Most implementations only treat it as the third.&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;a-received-invoice-is-a-file-someone-else-wrote&quot;&gt;A received invoice is a file someone else wrote&lt;/h2&gt;

&lt;p&gt;The naive implementation of “receive an XRechnung” looks something like this:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;upload_invoice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;tree&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;etree&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FILES&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;invoice&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;Invoice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tree&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;findtext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;.//cbc:ID&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;namespaces&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;total&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Decimal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tree&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;findtext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;.//cbc:PayableAmount&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;namespaces&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Every line of it is a problem.&lt;/p&gt;

&lt;p&gt;It parses arbitrary XML inside your web process, with whatever entity and DTD handling your parser defaults to. It trusts a file upload’s size. It throws away the original. It picks the first &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cbc:ID&lt;/code&gt; it finds, which may not be the invoice number. It turns the document into a model, so the moment someone edits that model, your database no longer says what your supplier sent you. And if anything after &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;etree.parse&lt;/code&gt; fails, nothing is stored at all: the supplier believes they delivered an invoice, and you have no trace of it.&lt;/p&gt;

&lt;p&gt;django-xrechnung’s inbound side is, to a large extent, the systematic removal of those assumptions. It is organized around a small number of rules.&lt;/p&gt;

&lt;h2 id=&quot;rule-1-commit-the-receipt-before-you-look-at-it&quot;&gt;Rule 1: Commit the receipt before you look at it&lt;/h2&gt;

&lt;p&gt;The first thing the app does with incoming bytes is store them. Not parse, not validate — store.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;checksum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hashlib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sha256&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;original&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hexdigest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# A committed row exists before any untrusted parsing or validator execution.
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transaction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;atomic&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;using&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;using&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;durable&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;created&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ReceivedDocument&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_or_create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;inbox&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;inbox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;sha256&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;checksum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;defaults&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;original&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ContentFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;original&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;receipt.bin&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;created&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;_attempt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;using&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;settings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;limits&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Everything that can go wrong afterwards — malformed XML, a validator that is down, a PDF that explodes when decompressed, a bug in my own code — happens &lt;em&gt;after&lt;/em&gt; a durable record exists that this document arrived, when, and in which inbox.&lt;/p&gt;

&lt;p&gt;That has a consequence that is easy to trip over: ingestion refuses to run inside an enclosing transaction. If your project sets &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ATOMIC_REQUESTS = True&lt;/code&gt;, the upload view must be non-atomic. Otherwise an exception during validation would roll back the receipt along with everything else, and you would be back to “the supplier sent it and we have no record”. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;durable=True&lt;/code&gt; flag turns that from a convention into an error.&lt;/p&gt;

&lt;p&gt;For developers, the general lesson is: &lt;strong&gt;separate admission from processing.&lt;/strong&gt; Admission must be cheap, bounded, and nearly impossible to fail once the bytes are in hand. Processing can fail as often as it likes, as long as it can be retried.&lt;/p&gt;

&lt;h2 id=&quot;rule-2-treat-invoices-as-hostile-input&quot;&gt;Rule 2: Treat invoices as hostile input&lt;/h2&gt;

&lt;p&gt;An invoice arrives from outside your organization, often by email, in a format designed for machines. That is a description of an attack surface.&lt;/p&gt;

&lt;p&gt;XML brings its classic problems: entity expansion, external entities, absurd nesting. PDFs bring their own: compression bombs, cyclic object trees, attachments inside attachments. And the official validator from KoSIT, the coordination office that publishes the XRechnung rules, is a Java program you have to start with the untrusted file as input.&lt;/p&gt;

&lt;p&gt;So all inspection in django-xrechnung runs in a separate, resource-limited worker process:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The XML parser is a pull parser with entity resolution, DTD loading and network access disabled. Depth, total bytes and the number of embedded attachments are counted while streaming, so a limit triggers before the whole tree exists in memory.&lt;/li&gt;
  &lt;li&gt;PDF extraction caps every decompression filter’s output and rejects compression filters it doesn’t expect.&lt;/li&gt;
  &lt;li&gt;The worker sets &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RLIMIT_AS&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RLIMIT_CPU&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RLIMIT_FSIZE&lt;/code&gt; on itself — in a fresh process, never via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preexec_fn&lt;/code&gt; inside a threaded Django server.&lt;/li&gt;
  &lt;li&gt;The parent drains stdout and stderr into bounded buffers and enforces a wall-clock deadline shared across all steps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The least glamorous part was killing processes correctly. The worker runs in its own session so that the whole process group can be terminated, including any children that kept the pipes open. But if you reap the child first, its pid can be recycled, and your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;killpg&lt;/code&gt; can hit an unrelated process. The fix is to check for exit with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;waitid(..., WNOWAIT)&lt;/code&gt;, which reports that the child exited without reaping it. It is a few lines of code, and exactly the kind of detail nobody gets right on the first attempt.&lt;/p&gt;

&lt;p&gt;None of this is specific to invoices. But invoices make it non-optional, because accepting them is now a legal obligation. You cannot simply refuse documents from senders you don’t trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For business readers:&lt;/strong&gt; this is also why django-xrechnung validates locally. Online validators are convenient, but every invoice you upload there discloses your supplier relationships, prices and bank details to a third party. Running the validator on your own infrastructure costs a Java installation. Not running it locally costs you control over who sees your purchasing data.&lt;/p&gt;

&lt;h2 id=&quot;rule-3-the-original-is-the-record&quot;&gt;Rule 3: The original is the record&lt;/h2&gt;

&lt;p&gt;German tax law requires invoices to be kept in the form in which they were received, for the statutory retention period, currently eight years. For an e-invoice, that form is the XML file — or the hybrid PDF with its embedded XML. A table of extracted fields is not the invoice. Neither is a nicely rendered HTML view of it.&lt;/p&gt;

&lt;p&gt;So received documents are immutable in django-xrechnung, and Django makes that easier to enforce than you might expect:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;EvidenceQuerySet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;models&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;QuerySet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ValidationError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Receipt evidence cannot be updated.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ValidationError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Receipt evidence cannot be deleted through the app.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ImmutableEvidence&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;models&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EvidenceQuerySet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;as_manager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;adding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ValidationError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Receipt evidence is immutable; append a new attempt.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;force_insert&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bulk_create&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bulk_update&lt;/code&gt; are blocked as well, foreign keys use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PROTECT&lt;/code&gt;, and the admin offers no edit or delete actions. The original bytes live in Django storage, so they can be backed up, replicated or moved to cold storage like any other file, and their SHA-256 stays in the database.&lt;/p&gt;

&lt;p&gt;It is important to be honest about what this does and doesn’t achieve. It prevents the &lt;em&gt;application&lt;/em&gt; from rewriting evidence by accident, which is where almost all real-world damage comes from. It doesn’t stop a database administrator, and the hashes are integrity metadata, not signatures. Documentation that claims more than the code delivers is its own compliance risk.&lt;/p&gt;

&lt;p&gt;The same principle extends to processing. A validation run is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ValidationAttempt&lt;/code&gt;, and attempts are immutable too. If the validator was misconfigured and you fix it, you retry, and the retry &lt;em&gt;appends&lt;/em&gt; a new attempt. The history of what you checked, when, with which validator and rule versions, is preserved.&lt;/p&gt;

&lt;h2 id=&quot;rule-4-decide-what-the-same-invoice-means&quot;&gt;Rule 4: Decide what “the same invoice” means&lt;/h2&gt;

&lt;p&gt;Suppliers resend invoices. Mail servers deliver twice. Someone uploads the same file again because they weren’t sure the first upload worked.&lt;/p&gt;

&lt;p&gt;django-xrechnung deduplicates by content: an inbox can contain a given sequence of bytes only once, enforced by a unique constraint on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(inbox, sha256)&lt;/code&gt;. Re-ingesting identical bytes returns the existing receipt with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;created=False&lt;/code&gt;, doesn’t reprocess it, and — just as important — doesn’t fire the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document_received&lt;/code&gt; signal again. To a host application listening for new invoices, a resubmission must not look like a second arrival.&lt;/p&gt;

&lt;p&gt;But byte identity is a deliberately narrow definition. A supplier who regenerates an invoice produces different bytes for what is, commercially, the same invoice: a new timestamp in the XML is enough. Recognizing &lt;em&gt;that&lt;/em&gt; as a duplicate requires business rules — same supplier, same invoice number, perhaps same amount — and those differ between organizations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For business readers:&lt;/strong&gt; byte-level deduplication protects you from technical duplicates. It does not protect you from paying the same invoice twice. That check belongs in your accounts-payable process, and it needs an owner.&lt;/p&gt;

&lt;h2 id=&quot;rule-5-four-outcomes-not-one&quot;&gt;Rule 5: Four outcomes, not one&lt;/h2&gt;

&lt;p&gt;The most important design decision on the inbound side was to stop asking “is this invoice OK?”&lt;/p&gt;

&lt;p&gt;Each processing attempt records four independent states:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Processing:&lt;/strong&gt; did the pipeline run to completion, or did a timeout, a missing validator or a resource limit stop it?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Validation:&lt;/strong&gt; do the official schema and business rules (KoSIT’s Schematron for XRechnung, or the Factur-X rules for that profile) accept the XML?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Parsing:&lt;/strong&gt; could the values be extracted unambiguously?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Reconciliation:&lt;/strong&gt; do the amounts add up?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of them can also be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unsupported&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unavailable&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;not_run&lt;/code&gt;, and those are deliberately different from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rejected&lt;/code&gt;. A validator that isn’t installed doesn’t make an invoice invalid. A profile the app doesn’t know yet isn’t a failed check. Collapsing all of that into one boolean is how systems end up rejecting good invoices whenever Java is missing — or, worse, accepting bad ones because a check silently didn’t happen.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“Valid” means the format is right. It doesn’t mean the invoice is correct, and it certainly doesn’t mean you should pay it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The KoSIT validator checks a great deal, including many arithmetic rules. But it can’t know whether you ordered these goods, whether the price matches your contract, or whether the bank account is the one your supplier has always used. That is not a failure of the validator. It is the boundary between format and business, and it is worth making visible in the software rather than letting “green checkmark” stand in for “approved”.&lt;/p&gt;

&lt;h2 id=&quot;rule-6-parse-into-snapshots-not-models&quot;&gt;Rule 6: Parse into snapshots, not models&lt;/h2&gt;

&lt;p&gt;Once a document is validated, it is tempting to load it into your own invoice model. django-xrechnung deliberately doesn’t. Parsed values are stored as a versioned JSON snapshot on the attempt, and received documents can never be turned into outgoing invoices or edited.&lt;/p&gt;

&lt;p&gt;Three details matter more than they look.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Values stay as they were written.&lt;/strong&gt; Amounts, quantities and dates are kept as the exact strings in the XML. Converting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;19.00&quot;&lt;/code&gt; to a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Decimal&lt;/code&gt; or a CII date like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;20261003&lt;/code&gt; to a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;date&lt;/code&gt; is something a consumer can do when it needs to. A snapshot that has already normalized values can no longer show what the supplier asserted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing is silently dropped.&lt;/strong&gt; EN 16931 is large, XRechnung adds to it, and real suppliers add more. The parser reads the fields it knows, and every leaf element and attribute it did &lt;em&gt;not&lt;/em&gt; consume is listed in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unmodeled&lt;/code&gt; together with its XPath. A tolerant parser that ignores what it doesn’t understand looks robust, but it hides information that could matter, such as a document-level discount you didn’t model. Listing what you didn’t read is cheap and makes the gap explicit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ambiguity is an error.&lt;/strong&gt; If a field that should occur once occurs twice, parsing is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rejected&lt;/code&gt; rather than taking the first value. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findtext()&lt;/code&gt; happily returns the first match. In an invoice, “the first match” can be the wrong number.&lt;/p&gt;

&lt;p&gt;For CII, I used the maintained &lt;a href=&quot;https://pypi.org/project/drafthorse/&quot;&gt;drafthorse&lt;/a&gt; reader to check the document, but take values from the original tree. Libraries that bind XML to objects tend to normalize on the way in, and you never want to serialize a received invoice back from a binding. The original XML stays authoritative; everything else is a view on it.&lt;/p&gt;

&lt;h2 id=&quot;rule-7-reconcile-what-you-can-and-say-what-you-cant&quot;&gt;Rule 7: Reconcile what you can, and say what you can’t&lt;/h2&gt;

&lt;p&gt;Reconciliation recalculates what can be recalculated: line net amounts from quantity and price, tax bases per VAT category, VAT per group, and document totals. It uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Decimal&lt;/code&gt; with cent rounding and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ROUND_HALF_UP&lt;/code&gt;, and records each comparison with the asserted and the expected value, so that a mismatch can be shown to a person rather than reported as “invalid”.&lt;/p&gt;

&lt;p&gt;Just as important is what it refuses to do. Document-level allowances, prepayments, rounding amounts, foreign currencies and VAT rates other than Germany’s 7% and 19% currently make reconciliation &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unsupported&lt;/code&gt;, with a reason. Factur-X MINIMUM and BASIC WL invoices don’t carry line details at all, so there is nothing to reconcile them against.&lt;/p&gt;

&lt;p&gt;A partial check never reports success. If I can check the lines but not the document-level discount, the honest answer is not “the lines matched”. It is “I couldn’t verify this invoice; here is why.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For business readers:&lt;/strong&gt; plan for an exception queue from day one. Some fraction of incoming invoices will be unsupported by any given tool — an unusual profile, a PDF without embedded XML, two XML attachments where there should be one. A system that tells you &lt;em&gt;which&lt;/em&gt; invoices need a human, and why, is far more valuable than one that claims to handle everything.&lt;/p&gt;

&lt;h2 id=&quot;the-pdf-is-a-picture-of-the-invoice&quot;&gt;The PDF is a picture of the invoice&lt;/h2&gt;

&lt;p&gt;Hybrid invoices deserve their own section, because they cause the most confusion.&lt;/p&gt;

&lt;p&gt;A ZUGFeRD or Factur-X invoice is a PDF that humans can read, with an XML file attached that machines can read. For tax purposes in Germany, the structured part is authoritative. The PDF is a rendering of it — ideally a faithful one.&lt;/p&gt;

&lt;p&gt;django-xrechnung extracts the embedded XML in the isolated worker, looking both in the PDF’s embedded-files tree and its associated files (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/AF&lt;/code&gt;), counting a file that appears in both only once. It requires exactly one XML candidate. If there are two, it doesn’t pick one by filename; it marks the document &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unsupported&lt;/code&gt; and asks for review. Encrypted PDFs and PDFs without XML are retained, but not processed. There is no OCR.&lt;/p&gt;

&lt;p&gt;And — this is the part I most want business readers to take away — the app does not check that the PDF &lt;em&gt;says the same thing&lt;/em&gt; as the XML. That is a hard problem, and a validated XML says nothing about it. If your team approves invoices by looking at the PDF while your accounting system books the XML, you have two versions of the truth and no one comparing them.&lt;/p&gt;

&lt;h2 id=&quot;pinning-the-rules-you-checked-against&quot;&gt;Pinning the rules you checked against&lt;/h2&gt;

&lt;p&gt;The XRechnung rules change. KoSIT publishes new validator configurations, Factur-X publishes new Schematron, and a document that passes today might not have passed a year ago, or vice versa.&lt;/p&gt;

&lt;p&gt;So django-xrechnung pins both. The KoSIT installation is verified against a manifest of file hashes; the Factur-X rules are pinned to a specific version of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;factur-x&lt;/code&gt; package and checked against bundled hashes before use. Every attempt records the validator version, the rule version and the hash of the scenario configuration it used.&lt;/p&gt;

&lt;p&gt;Running KoSIT turned out to be its own small project. It compiles every scenario’s Schematron at startup, which dominates the runtime of a single validation. Building the smallest configuration that serves each route — UBL Invoice, UBL CreditNote and CII for XRechnung, a single scenario per Factur-X profile — made a noticeable difference. For higher volumes, the app can talk to a long-running KoSIT daemon instead, which pays the startup cost once. The evidence then records that a daemon was used, because the application can’t inspect the configuration the operator started it with, and pretending otherwise would be a false claim.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For business readers:&lt;/strong&gt; “we validated it” is only meaningful together with “against which rules, on which date”. If an auditor asks in five years why you accepted an invoice, the report and the rule version you checked it against are your answer.&lt;/p&gt;

&lt;h2 id=&quot;what-i-would-tell-a-django-team-starting-this&quot;&gt;What I would tell a Django team starting this&lt;/h2&gt;

&lt;p&gt;If you are about to build e-invoice receiving into a Django project, or deciding whether to adopt something like django-xrechnung, these are the points I would put on the whiteboard:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Store first, process second.&lt;/strong&gt; The receipt must commit before any untrusted parsing begins. Watch out for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ATOMIC_REQUESTS&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Isolate parsing and validation.&lt;/strong&gt; Separate process, resource limits, no entity resolution, bounded output. Accepting invoices is mandatory; trusting them isn’t.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Make evidence immutable in the ORM.&lt;/strong&gt; Override &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;save&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete&lt;/code&gt; and the queryset’s bulk operations, use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PROTECT&lt;/code&gt;, and append rather than update.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Keep outcomes separate.&lt;/strong&gt; Processing, validation, parsing and reconciliation answer different questions. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unsupported&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unavailable&lt;/code&gt; are not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rejected&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Snapshot, don’t import.&lt;/strong&gt; Keep asserted values as strings, list what you didn’t read, and reject ambiguity instead of guessing.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Decimal&lt;/code&gt; and say what you didn’t check.&lt;/strong&gt; A partial reconciliation must never look like a pass.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Pin and record your rules.&lt;/strong&gt; The validator version is part of the evidence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And for the business side:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Receiving is already mandatory.&lt;/strong&gt; If your process still assumes invoices arrive as PDFs to be typed in, it is behind the law, not ahead of it.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The XML is the invoice.&lt;/strong&gt; Approve what you book, and be aware of when people look at the PDF instead.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Valid isn’t payable.&lt;/strong&gt; Format validation removes one class of error. Contract, delivery and bank-detail checks are still yours.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Expect exceptions, and staff them.&lt;/strong&gt; The right tool tells you which invoices need a person and why.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Keep the original and the evidence.&lt;/strong&gt; Eight years is a long time to explain a decision without records.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Know where your invoices go.&lt;/strong&gt; Validating locally keeps purchasing data inside your organization.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;why-this-belongs-in-a-reusable-application&quot;&gt;Why this belongs in a reusable application&lt;/h2&gt;

&lt;p&gt;None of the rules above is exotic. But each of them takes time to discover, usually after something has gone wrong: the receipt that disappeared in a rolled-back transaction, the duplicate that was paid twice, the validator outage that marked a week’s worth of invoices as invalid.&lt;/p&gt;

&lt;p&gt;Every German business running on Django will need to solve this problem, and most of them will be solving it for the first time. That is exactly the situation I described in &lt;a href=&quot;/writing/stop-rebuilding-apps/&quot;&gt;Stop Rebuilding the Same Django App for Every Client&lt;/a&gt;: recurring knowledge that should accumulate in software instead of being rediscovered project by project.&lt;/p&gt;

&lt;p&gt;django-xrechnung is open source under the BSD license, currently in beta, and its &lt;a href=&quot;https://django-xrechnung.readthedocs.io/en/stable/how-to/receive-documents.html&quot;&gt;documentation&lt;/a&gt; covers the receiving workflow in detail. Its scope is still limited — German parties, EUR, and a defined set of profiles — and it says so in its &lt;a href=&quot;https://django-xrechnung.readthedocs.io/en/stable/reference/supported-invoices.html&quot;&gt;supported-invoice reference&lt;/a&gt; rather than pretending otherwise.&lt;/p&gt;

&lt;p&gt;If you are receiving e-invoices in Django, I would like to hear what arrives in your inbox. Especially the invoices that don’t fit.&lt;/p&gt;
</content>
    
  </entry>
  
  
  <entry>
    <title>Open Source Is Dying Again</title>
    
    <link href="https://www.fabian-braun.de/writing/open-source-is-dying-again/" rel="alternate" type="text/html"/>
    
    <id>https://www.fabian-braun.de/writing/open-source-is-dying-again/</id>
    <published>2026-09-28T00:00:00+02:00</published>
    <updated>2026-09-28T00:00:00+02:00</updated>
    <category term="software-reuse"/>
    <summary>AI is breaking the reciprocity that kept open source going — through licenses, reputation, and the cost of forking. What remains is the reciprocity that was always worth the most: passing knowledge on.</summary>
    
    <content type="html" xml:base="https://www.fabian-braun.de/writing/open-source-is-dying-again/">&lt;p&gt;Open source has died many times already.&lt;/p&gt;

&lt;p&gt;It was supposed to die when large companies decided it was a threat. It was supposed to die when those same companies decided it was a business model. It was supposed to die when cloud providers started offering managed versions of projects they did not maintain, and again when projects responded by switching to licenses that were no longer open. Every few years, someone discovers that the maintainers of critical infrastructure are unpaid and exhausted, and the obituaries get written once more.&lt;/p&gt;

&lt;p&gt;And yet the ecosystem kept growing.&lt;/p&gt;

&lt;p&gt;So I am suspicious of anyone announcing the end of open source. But I also think this time is different, because the thing that is changing is not a business model or a license. It is the cost of the work itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI has not killed open source. It has killed the bargain that open source contributors thought they had made.&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;the-old-bargain&quot;&gt;The old bargain&lt;/h2&gt;

&lt;p&gt;Nobody ever wrote the bargain down, but most contributors understood it.&lt;/p&gt;

&lt;p&gt;You publish your work. Others may use it, study it, change it, and often build businesses on it. In return, a few things hold. Your name stays attached to the work. The license sets the terms under which others reuse it. And the quality of your contributions becomes visible: you build a reputation, and that reputation turns into jobs, invitations, clients, and influence.&lt;/p&gt;

&lt;p&gt;None of this was ever guaranteed. But it was stable enough that people chose to take part.&lt;/p&gt;

&lt;p&gt;At its core, the bargain was about reciprocity, though never of the one-to-one kind. Nobody paid back the specific person whose library they used. You took from the commons and gave back to the commons. Anthropologists call this generalized reciprocity: it works across a whole community rather than between two parties. That is why it survived obvious free-riding. Enough people gave back, and everyone was also a user of everyone else’s work.&lt;/p&gt;

&lt;p&gt;Open source never relied on goodwill alone to keep that going. Licenses made reciprocity a legal obligation, at least for attribution and, with copyleft, for sharing changes. Reputation made it visible. And economics made it sensible.&lt;/p&gt;

&lt;p&gt;Each of those assumptions is now under pressure.&lt;/p&gt;

&lt;h2 id=&quot;copying-has-become-cheap&quot;&gt;Copying has become cheap&lt;/h2&gt;

&lt;p&gt;Open source always depended on a subtle economic fact: even with the source code available, reproducing a mature project was expensive.&lt;/p&gt;

&lt;p&gt;You could read every line of Django. That did not mean you could build a comparable framework over a weekend. The value was not only in the code but in the thousands of decisions embedded in it, and understanding those decisions took time. So it was usually cheaper to use the project, and often to contribute back, than to rebuild it.&lt;/p&gt;

&lt;p&gt;That cost has collapsed.&lt;/p&gt;

&lt;p&gt;A model trained on public code can produce something that looks very much like the solution a project arrived at over years, adapted to a slightly different context, in minutes. The patterns, the idioms, the carefully chosen boundaries are all available as raw material. Work that once required a developer to understand and reproduce an implementation can increasingly be generated from a description of the desired result.&lt;/p&gt;

&lt;p&gt;The incentive to use the original, and with it the incentive to contribute to it, becomes weaker.&lt;/p&gt;

&lt;p&gt;This matters more than it seems, because contributing back was rarely pure altruism. Companies upstreamed their patches because maintaining a private fork was expensive. Every new release meant merging their changes again, so giving the changes to the project was the cheaper path. Reciprocity was, to a large extent, simply the rational choice.&lt;/p&gt;

&lt;p&gt;AI lowers the cost of maintaining divergent code as well. If a private variant can be regenerated or re-adapted cheaply, the economic pressure to contribute back weakens. Copyleft enforced reciprocity through law, the cost of forking enforced it through economics. Both are eroding at the same time.&lt;/p&gt;

&lt;h2 id=&quot;licenses-without-effect&quot;&gt;Licenses without effect&lt;/h2&gt;

&lt;p&gt;Licenses were the legal backbone of the bargain. Even the most permissive ones usually ask for one thing: keep the copyright notice, credit the authors.&lt;/p&gt;

&lt;p&gt;AI-generated code does not do that. It does not reproduce a project; it reproduces what it learned from many projects, recombined. There is no notice to keep, because there is no identifiable copy. The attribution requirement, the one condition nearly all open source licenses share, simply has nothing to attach to.&lt;/p&gt;

&lt;p&gt;Copyleft licenses face the same problem in a sharper form. Their strength was that derived work had to remain open. But if the derivation passes through a model, is the output derived work? Maybe. Probably, in some cases. Proving it is another matter.&lt;/p&gt;

&lt;h2 id=&quot;rights-you-cannot-enforce&quot;&gt;Rights you cannot enforce&lt;/h2&gt;

&lt;p&gt;Even where a claim exists, enforcing it is close to impossible.&lt;/p&gt;

&lt;p&gt;To enforce a license, you need to show that your work was copied, by whom, and in violation of which terms. With AI-generated code, the copying happens inside a training process you cannot inspect, and the output appears in codebases you will never see. The individual maintainer has no realistic way to find out, and even less of a way to pursue it.&lt;/p&gt;

&lt;p&gt;Rights that cannot be enforced are, in practice, suggestions.&lt;/p&gt;

&lt;h2 id=&quot;reputation-that-never-arrives&quot;&gt;Reputation that never arrives&lt;/h2&gt;

&lt;p&gt;For many contributors, reputation mattered more than licenses. Very few people have ever sued over open source. Many people built careers on it.&lt;/p&gt;

&lt;p&gt;That is eroding too. When someone uses a well-known library directly, they see the name of the project and, sometimes, its authors. When someone receives an AI-generated implementation of the same idea, they see nothing. The knowledge still travels. The credit does not.&lt;/p&gt;

&lt;p&gt;The work still matters. It just no longer makes the person who did it visible.&lt;/p&gt;

&lt;p&gt;Seen together, licenses and reputation served the same purpose: they made reciprocity traceable. They connected a piece of work to the people who created it, so that credit, obligations, and opportunities could flow back to them. Once that trace disappears, reciprocity has nowhere to go. You cannot give back to someone you don’t know you took from.&lt;/p&gt;

&lt;h2 id=&quot;someone-always-made-the-money&quot;&gt;Someone always made the money&lt;/h2&gt;

&lt;p&gt;It would be easy to stop here and conclude that open source is finished. I don’t think that is right, because the bargain was never as fair as we pretended.&lt;/p&gt;

&lt;p&gt;Someone has always been making money from open source work, and it was rarely the people doing the work. Companies built products on top of volunteer projects. Cloud providers sold hosted versions of software they did not write. Consultants and agencies, myself included, earned a living by applying open source to their clients’ problems. Most maintainers received a fraction of the value their work created, if anything at all.&lt;/p&gt;

&lt;p&gt;AI makes this imbalance more visible and more extreme. But it did not invent it.&lt;/p&gt;

&lt;p&gt;It also concentrates it. For more than a decade, open source has effectively meant GitHub, and the company that hosts much of the world’s public code is now also selling AI coding tools trained on public code. The place where the work is shared and the business that profits from learning from it are no longer clearly separate. Moving to community-run forges such as &lt;a href=&quot;https://codeberg.org&quot;&gt;Codeberg&lt;/a&gt;, as I am currently doing with my personal repositories, does not guarantee to take any code out of a training set. But it keeps the infrastructure of open source under the governance of the people who use it.&lt;/p&gt;

&lt;p&gt;To be fair, contributors do get something back. They use AI tools trained on the commons too, and those tools make them more productive. But that return is mediated and monetized: developers get access to what the commons taught the model, as a subscription. It is still a form of reciprocity, but the relationship has become commercial, and the terms are set by someone else.&lt;/p&gt;

&lt;p&gt;What AI changes is &lt;em&gt;which kind&lt;/em&gt; of work can be captured cheaply by others. And that points to where contributors should put their effort.&lt;/p&gt;

&lt;h2 id=&quot;from-coder-to-architect&quot;&gt;From coder to architect&lt;/h2&gt;

&lt;p&gt;What becomes cheap is implementation: producing working code for a problem that is already understood.&lt;/p&gt;

&lt;p&gt;What does not become cheap is understanding the problem in the first place. Deciding what a system should do and, more importantly, what it should not do. Finding the boundaries that allow a codebase to evolve for a decade. Knowing which abstraction will hold and which one will quietly become a liability. Judging whether a contribution fits a project’s direction, even when it looks perfectly reasonable in isolation.&lt;/p&gt;

&lt;p&gt;That is architectural work, and it is increasingly the scarce part.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;When code is abundant, architecture counts.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The role of the open source contributor is shifting accordingly: from writing code to shaping systems. From producing implementations to making the decisions that implementations depend on. AI can generate a pull request. It cannot yet take responsibility for where a project is going, or for the community of people who depend on it.&lt;/p&gt;

&lt;h2 id=&quot;twenty-years-of-django&quot;&gt;Twenty years of Django&lt;/h2&gt;

&lt;p&gt;Django is a good illustration of what this means in practice.&lt;/p&gt;

&lt;p&gt;It was released as open source in 2005, and it is still one of the most widely used web frameworks two decades later. Comparatively little of its original code survives. The ORM, the template engine, the admin, request handling, and most other parts have been rewritten, some of them more than once. What survived is the architecture: the decisions about how the parts relate to each other.&lt;/p&gt;

&lt;p&gt;Many of those decisions were made very deliberately, and some are written down in Django’s &lt;a href=&quot;https://docs.djangoproject.com/en/stable/misc/design-philosophies/&quot;&gt;design philosophies&lt;/a&gt;. Loose coupling between layers. A template language intentionally limited so that business logic stays out of templates. Projects composed of reusable applications, so that functionality can be shared between projects instead of rebuilt for each one. A strict deprecation policy, so that the framework can change without breaking its users every release.&lt;/p&gt;

&lt;p&gt;Not every decision was right in hindsight. Early versions contained a good deal of “magic” that had to be removed again. Some abstractions aged better than others, and some are carried along today mainly because removing them would cost more than keeping them.&lt;/p&gt;

&lt;p&gt;But the architecture held. It absorbed new versions of Python, a migrations framework, class-based views, a redesigned middleware system, and eventually asynchronous support, without forcing its users to start over. That is not the result of writing a lot of code. It is the result of a community that has spent twenty years carefully deciding which code to write, and which not.&lt;/p&gt;

&lt;p&gt;An AI can reproduce Django’s code. What it cannot reproduce is the process that produced it: the discussions, the trade-offs, the rejected proposals, and the people who carry the reasons forward.&lt;/p&gt;

&lt;h2 id=&quot;where-people-grow-into-that-role&quot;&gt;Where people grow into that role&lt;/h2&gt;

&lt;p&gt;Here is the part I find encouraging.&lt;/p&gt;

&lt;p&gt;Architectural judgment is hard to learn in isolation. You learn it by working on real systems with a long history, alongside people who have already made many of the mistakes. Most organizations cannot offer that to more than a handful of people.&lt;/p&gt;

&lt;p&gt;Open source can.&lt;/p&gt;

&lt;p&gt;Mature open source projects are among the few places where anyone can study how long-lived software actually evolves, participate in design discussions, see why decisions were made, and receive feedback from experienced maintainers. That has always been true, but it used to be a side effect of contributing code. Now it may be the main reason to contribute at all.&lt;/p&gt;

&lt;p&gt;Programs like &lt;a href=&quot;https://djangonaut.space&quot;&gt;Djangonaut Space&lt;/a&gt; show what this can look like. They don’t just help people land their first pull request. They pair new contributors with experienced navigators, give them context on how a project works, and help them understand not only &lt;em&gt;what&lt;/em&gt; to change but &lt;em&gt;why&lt;/em&gt;. That is exactly the kind of knowledge AI does not replace, and exactly the kind of growth that makes contributors more valuable, not less.&lt;/p&gt;

&lt;p&gt;It is also reciprocity in its most durable form. Navigators pass on what they once received from the people who mentored them. Open source reciprocity was never really about paying back the person you took from. It was about passing it on. That form of reciprocity doesn’t need a license or an attribution notice, and no model can perform it on anyone’s behalf.&lt;/p&gt;

&lt;h2 id=&quot;what-is-dying-and-what-isnt&quot;&gt;What is dying, and what isn’t&lt;/h2&gt;

&lt;p&gt;So is open source dying?&lt;/p&gt;

&lt;p&gt;The version in which the value of a contribution lay mainly in the code, protected by a license and rewarded with reputation, is in serious trouble. I don’t think it is coming back.&lt;/p&gt;

&lt;p&gt;But open source was never only a pile of code. It was also a place where people learned to build software together, where knowledge about how systems should be designed was passed on, and where judgment was developed in public.&lt;/p&gt;

&lt;p&gt;That part is not dying. If anything, it has become more important.&lt;/p&gt;

&lt;p&gt;The code will be copied. The understanding behind it still has to be earned.&lt;/p&gt;
</content>
    
  </entry>
  
  
  <entry>
    <title>What If Brute Force Produces Insight?</title>
    
    <link href="https://www.fabian-braun.de/writing/what-if-brute-force-produces-insight/" rel="alternate" type="text/html"/>
    
    <id>https://www.fabian-braun.de/writing/what-if-brute-force-produces-insight/</id>
    <published>2026-09-19T00:00:00+02:00</published>
    <updated>2026-09-19T00:00:00+02:00</updated>
    <category term="automation-ai"/>
    <summary>AI, mathematical discovery, and what remains when searching for answers becomes cheap.</summary>
    
    <content type="html" xml:base="https://www.fabian-braun.de/writing/what-if-brute-force-produces-insight/">&lt;p&gt;OpenAI recently reported that a system involving on the order of 10,000 concurrent AI agents produced a proposed resolution of the Navier–Stokes Millennium Prize Problem. According to &lt;a href=&quot;https://openai.com/index/navier-stokes-solution/&quot;&gt;OpenAI’s account of the experiment&lt;/a&gt;, the effort took about 88 hours, involved 2.7 million agent messages and roughly 130 billion output tokens, followed by another 17 hours of Lean formalization and verification.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://www.claymath.org/news/navier-stokes-announcement/&quot;&gt;Clay Mathematics Institute describes the problem as “apparently” settled&lt;/a&gt;, while emphasizing that its evaluation process is deliberately unhurried. So there is good reason not to treat the story as finished yet.&lt;/p&gt;

&lt;p&gt;Still, my first reaction was: that’s an extraordinary amount of brute force. My second reaction was: does it matter?&lt;/p&gt;

&lt;p&gt;If the resulting mathematics withstands scrutiny, the proof can be inspected independently of the process that discovered it. We can ask whether each step follows, whether its assumptions are valid, and whether its conclusion is justified. The proof doesn’t remember how it was found.&lt;/p&gt;

&lt;h2 id=&quot;search-and-insight&quot;&gt;Search and Insight&lt;/h2&gt;

&lt;p&gt;Some of the most beautiful results in mathematics are interesting because they reveal unexpected structure. &lt;a href=&quot;https://plato.stanford.edu/entries/goedel-incompleteness/&quot;&gt;Gödel’s incompleteness theorems&lt;/a&gt;, for example, establish fundamental limits on what sufficiently expressive consistent formal systems can prove. Their importance doesn’t come from someone having performed a particularly large calculation. They changed how we understand formal mathematics.&lt;/p&gt;

&lt;p&gt;That makes massive AI searches feel fundamentally different. Thousands of agents exploring possibilities sounds less like mathematical insight and more like an enormous search procedure. But perhaps the distinction isn’t as clear as it seems.&lt;/p&gt;

&lt;p&gt;Humans traditionally do both: we search and we compress. We try approaches, discard them, notice connections, and eventually find an argument that reduces all that exploration to something another person can understand.&lt;/p&gt;

&lt;p&gt;AI may dramatically reduce the cost of the search. OpenAI describes its agents exploring different approaches in parallel and later cross-pollinating promising intermediate results between groups. If systems can do this at enormous scale, vastly more mathematical territory becomes accessible.&lt;/p&gt;

&lt;p&gt;The enormous search can disappear once a compact argument has been found.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Perhaps brute force isn’t the opposite of insight. It can be a way of finding it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;does-it-matter-how-we-got-there&quot;&gt;Does It Matter How We Got There?&lt;/h2&gt;

&lt;p&gt;Imagine two mathematicians independently produce the same elegant proof. One arrives at it after years of intuition and careful thought. The other uses a program that searches millions of possibilities until it eventually produces the argument. The discovery processes are completely different; the resulting mathematics may be identical.&lt;/p&gt;

&lt;p&gt;Of course, mathematics is more than correctness. We value explanations, useful concepts, connections, and proofs that reveal &lt;em&gt;why&lt;/em&gt; something is true. But those qualities can exist in the resulting proof regardless of how messy the path toward it was.&lt;/p&gt;

&lt;p&gt;An ugly search can discover a beautiful argument.&lt;/p&gt;

&lt;p&gt;This becomes more interesting when AI finds something we didn’t expect. Proving a result mathematicians already strongly suspected is one thing. Producing a valid proof of something that overturns prevailing intuition is another.&lt;/p&gt;

&lt;p&gt;Take the &lt;a href=&quot;https://www.claymath.org/millennium/p-vs-np/&quot;&gt;P versus NP problem&lt;/a&gt;. Most computer scientists expect (P \neq NP), but nobody has proved it. Imagine a machine-generated proof establishing (P = NP) instead. Humans inspect it, formal systems verify it, and nobody finds an error.&lt;/p&gt;

&lt;p&gt;At that point, saying it was “only brute force” would miss what happened. An enormous search would have produced a result that changed our understanding of computation.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The brute force would not merely have found the proof. It would have surprised us.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;search-is-moving-up-the-stack&quot;&gt;Search Is Moving Up the Stack&lt;/h2&gt;

&lt;p&gt;I’ve been thinking about a related question in software development. AI is clearly reducing the cost of implementation, which suggests that scarce human work moves upward toward intent, context, architecture, and judgment.&lt;/p&gt;

&lt;p&gt;AI doing mathematics complicates that picture because it isn’t merely executing a path somebody else specified. Increasingly, it can also search for the path. In the Navier–Stokes effort, the agents were deliberately given different approaches to explore, with intermediate findings later fed into further searches.&lt;/p&gt;

&lt;p&gt;That suggests a progression:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;execution
    ↓
search for solutions
    ↓
verification
    ↓
interpretation
    ↓
choosing worthwhile problems
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We shouldn’t assume that AI stops permanently at any particular line. But as execution and search become cheaper, the value of the remaining layers changes.&lt;/p&gt;

&lt;p&gt;For mathematics, verification still matters. OpenAI has also &lt;a href=&quot;https://github.com/openai/NavierStokesAndEuler&quot;&gt;published the Lean formalizations&lt;/a&gt; and instructions for independently checking them. But verification isn’t the end of the work.&lt;/p&gt;

&lt;p&gt;If a machine really proved (P = NP), for example, knowing that the proof is correct would only begin the next phase. We would want to understand why our intuition had been wrong, what follows from the result, which existing assumptions need to be reconsidered, and what new questions suddenly become interesting.&lt;/p&gt;

&lt;p&gt;A proof can establish that something is true without telling us what the result means.&lt;/p&gt;

&lt;p&gt;And before all of that comes another question: why were we looking there in the first place?&lt;/p&gt;

&lt;h2 id=&quot;what-remains-scarce&quot;&gt;What Remains Scarce?&lt;/h2&gt;

&lt;p&gt;I don’t find the question “Can AI have insight?” particularly useful. It depends too much on what we mean by insight and what we believe happens internally in humans or machines.&lt;/p&gt;

&lt;p&gt;A more practical question is:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;What remains scarce when machines can search enormous spaces of possible solutions?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Today, choosing worthwhile problems, understanding why they matter, interpreting results, and connecting them to everything else we know still look like unusually valuable forms of judgment. Perhaps AI will become good at those things too. I don’t see a strong reason to assume otherwise.&lt;/p&gt;

&lt;p&gt;That is why the interesting consequence of AI doing mathematics may not be that machines are becoming mathematicians. It may be that &lt;strong&gt;search and insight were less cleanly separated than we thought&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If AI keeps making both execution and search cheaper, the useful question for humans is no longer simply what machines cannot do. It becomes:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Which questions are worth asking, which answers matter, and what do we understand differently once the search is no longer ours alone?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content>
    
  </entry>
  
  
  <entry>
    <title>Stop Rebuilding the Same Django App for Every Client</title>
    
    <link href="https://www.fabian-braun.de/writing/stop-rebuilding-apps/" rel="alternate" type="text/html"/>
    
    <id>https://www.fabian-braun.de/writing/stop-rebuilding-apps/</id>
    <published>2026-09-09T00:00:00+02:00</published>
    <updated>2026-09-09T00:00:00+02:00</updated>
    <category term="software-reuse"/>
    <summary>How reusable Django applications can turn agency project work into accumulating product knowledge.</summary>
    
    <content type="html" xml:base="https://www.fabian-braun.de/writing/stop-rebuilding-apps/">&lt;p&gt;Agencies build different websites for different clients, but they don’t always build different applications. One corporate website needs people, another needs locations, another needs events. Others need services, jobs, case studies, publications, or projects.&lt;/p&gt;

&lt;p&gt;The details differ, but the underlying concepts often don’t. And yet we frequently build them again: a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Person&lt;/code&gt; model for one customer, another &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Person&lt;/code&gt; model for the next, with different CMS integration, templates, multilingual setup, and publishing logic.&lt;/p&gt;

&lt;p&gt;After five projects, the agency has implemented People five times. It has completed five projects — but has it built an asset?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build the domain application once. Adapt it to the client.&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;the-repetition-is-easy-to-miss&quot;&gt;The Repetition Is Easy to Miss&lt;/h2&gt;

&lt;p&gt;Take something as ordinary as a team section. The first client needs:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Person
├── name
├── role
├── photo
└── biography
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The next client also needs departments and locations. A third needs multiple languages, while a fourth needs drafts and publishing. The fifth wants people to appear on team pages, office pages, articles, and landing pages.&lt;/p&gt;

&lt;p&gt;These projects are different, but much of what we’re building is not. We still have a person with relationships, URLs, templates, perhaps an API, and because this is a CMS project, we repeatedly need the same editorial capabilities around it:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;localization&lt;/li&gt;
  &lt;li&gt;versioning&lt;/li&gt;
  &lt;li&gt;preview&lt;/li&gt;
  &lt;li&gt;publishing&lt;/li&gt;
  &lt;li&gt;frontend editing&lt;/li&gt;
  &lt;li&gt;permissions&lt;/li&gt;
  &lt;li&gt;composition into pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The recurring cost isn’t only the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Person&lt;/code&gt; model. It is everything required to turn that model into something editors can actually work with.&lt;/p&gt;

&lt;h2 id=&quot;project-reuse-is-not-product-reuse&quot;&gt;Project Reuse Is Not Product Reuse&lt;/h2&gt;

&lt;p&gt;Of course, developers already reuse code. We copy models and plugins, keep project templates, and borrow snippets from previous projects. That helps, but copying an implementation is different from maintaining a reusable application.&lt;/p&gt;

&lt;p&gt;When I copy the People implementation from Client A into Client B, the two versions immediately begin to diverge. A bug fixed for Client B doesn’t automatically improve Client A. A better editorial workflow discovered on Client C doesn’t become available to the others, and a useful API added for Client D remains a Client D feature.&lt;/p&gt;

&lt;p&gt;The agency keeps learning. The software doesn’t.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if recurring project knowledge accumulated in reusable Django applications instead?&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;django-already-has-the-right-unit-of-reuse&quot;&gt;Django Already Has the Right Unit of Reuse&lt;/h2&gt;

&lt;p&gt;Django has had an answer to this problem from the beginning: applications.&lt;/p&gt;

&lt;p&gt;A People application can own the things that belong to the People domain:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;people/
├── models.py
├── urls.py
├── views.py
├── api.py
├── templates/
└── ...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It can define what a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Person&lt;/code&gt; is, relationships between people, departments and locations, APIs, permissions, and business logic. None of this needs to belong to the CMS.&lt;/p&gt;

&lt;p&gt;This is the same distinction I argued for in &lt;a href=&quot;https://www.django-cms.org/resources/blog/2026/08/24/the-cms-is-not-the-application/&quot;&gt;The CMS Is Not the Application&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The application should own the architecture. The CMS should make it editable.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The difficulty has traditionally been the second half. Making a normal Django application feel like first-class content inside a CMS has required enough integration work that rebuilding the feature inside each project can still feel easier.&lt;/p&gt;

&lt;p&gt;That is the problem I’ve been trying to reduce with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;djangocms-custom-content&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;make-the-application-editable&quot;&gt;Make the Application Editable&lt;/h2&gt;

&lt;p&gt;Suppose we start with a reusable People application. Its domain might be very small:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;models&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;slug&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;models&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SlugField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;unique&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then we add the editorial content that needs to vary by language and publishing state:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PersonContent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;models&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;person&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;models&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ForeignKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;on_delete&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;models&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CASCADE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;language&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;models&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CharField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;max_length&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;15&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;models&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CharField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;max_length&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;role&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;models&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CharField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;max_length&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;biography&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;models&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TextField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;What we want from the CMS is not ownership of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Person&lt;/code&gt;. We want capabilities around it: make the content multilingual and versioned, give editors preview and frontend editing, allow people to be composed into pages, and provide a useful administration interface.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Person&lt;/code&gt; remains part of the Django application. The CMS makes it manageable as content.&lt;/p&gt;

&lt;p&gt;That distinction is what makes reuse interesting.&lt;/p&gt;

&lt;h2 id=&quot;one-application-different-clients&quot;&gt;One Application, Different Clients&lt;/h2&gt;

&lt;p&gt;Client A can use the People application with a simple team page. Client B can add departments, Client C can relate people to offices, Client D can expose them through an API to another frontend, and Client E can use the same people in articles, case studies, and landing pages.&lt;/p&gt;

&lt;p&gt;The projects don’t have to become identical. The reusable application provides a stable domain and a set of capabilities, while individual projects remain free to compose and extend them.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;                    People
                      │
          ┌───────────┼───────────┐
          │           │           │
      Client A    Client B    Client C
          │           │           │
       Team       Locations      API
       pages       + teams     + website
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A useful improvement to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;People&lt;/code&gt; can now flow back into the reusable application instead of remaining trapped inside one customer project.&lt;/p&gt;

&lt;p&gt;Over time, the agency isn’t merely delivering projects. It is accumulating capabilities.&lt;/p&gt;

&lt;h2 id=&quot;from-projects-to-a-capability-portfolio&quot;&gt;From Projects to a Capability Portfolio&lt;/h2&gt;

&lt;p&gt;Once you start looking at agency work this way, People is only the beginning. Many agencies repeatedly build some variation of:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;people/
events/
locations/
services/
jobs/
projects/
case_studies/
publications/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Not every agency needs the same applications, and there shouldn’t be one universal model for an Event or Service that satisfies every website. The point is not to standardize every client. The point is that an agency can decide where &lt;strong&gt;its own recurring knowledge&lt;/strong&gt; deserves to become software.&lt;/p&gt;

&lt;p&gt;Perhaps after building event systems for four customers, you know what your Event application should look like. Perhaps your agency has a particularly good model for Locations, or understands publishing organizations and has developed excellent applications for Authors, Publications, and Topics.&lt;/p&gt;

&lt;p&gt;That knowledge can become part of your capability portfolio. The next project starts from there rather than from zero.&lt;/p&gt;

&lt;h2 id=&quot;reuse-changes-the-economics-of-improvement&quot;&gt;Reuse Changes the Economics of Improvement&lt;/h2&gt;

&lt;p&gt;Suppose improving the People application takes two days. If it belongs to one client project, the improvement needs to justify two days for that client. If the application is used across ten projects, the economics change.&lt;/p&gt;

&lt;p&gt;Accessibility improvements benefit ten implementations. A better editing experience benefits ten editorial teams. Compatibility work benefits ten projects. A bug fix happens once, and a new CMS capability becomes available everywhere the application is used.&lt;/p&gt;

&lt;p&gt;The cost of making the shared capability better is spread across the value it creates.&lt;/p&gt;

&lt;p&gt;That is how product development works. Agency work usually doesn’t — but perhaps the boundary doesn’t need to be as sharp as we assume.&lt;/p&gt;

&lt;h2 id=&quot;reusable-does-not-mean-generic&quot;&gt;Reusable Does Not Mean Generic&lt;/h2&gt;

&lt;p&gt;There is an obvious danger here. Once developers discover reuse, we tend to generalize. Our simple People application becomes configurable, then highly configurable, then it needs twenty settings because every previous customer did something slightly different.&lt;/p&gt;

&lt;p&gt;Eventually we have created a generic framework nobody particularly enjoys using.&lt;/p&gt;

&lt;p&gt;That’s not what I’m proposing. A reusable application should still have opinions and represent a useful model of its domain. Projects can extend it when necessary, and sometimes a client’s requirements really are different enough that a separate implementation is the right answer.&lt;/p&gt;

&lt;p&gt;The goal isn’t maximum reuse. Just as with component architecture, maximum flexibility isn’t automatically a virtue.&lt;/p&gt;

&lt;p&gt;The goal is to identify the things we genuinely build repeatedly and allow the knowledge behind them to accumulate.&lt;/p&gt;

&lt;h2 id=&quot;this-is-what-im-exploring-with-djangocms-custom-content&quot;&gt;This Is What I’m Exploring with djangocms-custom-content&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;djangocms-custom-content&lt;/code&gt; is infrastructure for experimenting with this model. It tries to make the CMS-specific part of a reusable Django application inexpensive enough that the application can remain the primary architectural unit.&lt;/p&gt;

&lt;p&gt;A normal Django domain can gain multilingual editorial content, versioning, preview, frontend editing, CMS administration, application URLs, and relationships between versioned and ordinary Django objects without having to become a special-purpose CMS architecture.&lt;/p&gt;

&lt;p&gt;I’ve included applications such as People, Services, and Categories as examples, not because I think django CMS should own universal implementations of those concepts. Quite the opposite: they are there to explore how little CMS-specific machinery should be necessary for developers to build their own reusable applications.&lt;/p&gt;

&lt;p&gt;The framework is still young. The interesting test now isn’t whether I can add more features to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is whether other developers and agencies recognize the problem.&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;what-do-you-keep-rebuilding&quot;&gt;What Do You Keep Rebuilding?&lt;/h2&gt;

&lt;p&gt;That’s the question I’m most interested in. If you build Django or django CMS projects for multiple clients, which applications have you built more than once?&lt;/p&gt;

&lt;p&gt;People? Events? Locations? Services? Jobs? Or something much more specific to the industries you work with?&lt;/p&gt;

&lt;p&gt;And if you’ve tried turning those implementations into reusable Django applications, what stopped the reuse from working? Were customers’ domains genuinely too different? Was extension too difficult? Did CMS integration create too much project-specific plumbing? Did maintaining a reusable package cost more than rebuilding it?&lt;/p&gt;

&lt;p&gt;Those answers matter more to me right now than another feature request. Because if the recurring problem is real, the opportunity isn’t merely to make custom content easier. It’s to change what accumulates when we build client projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agency should keep more than the experience. The software should learn too.&lt;/strong&gt;&lt;/p&gt;
</content>
    
  </entry>
  
  
  <entry>
    <title>Plugin Architecture Is Editor Experience</title>
    
    <link href="https://www.django-cms.org/resources/blog/2026/09/07/plugin-architecture-is-editor-experience/" rel="alternate" type="text/html"/>
    
    <id>https://www.fabian-braun.de/writing/plugin-architecture-is-editor-experience/</id>
    <published>2026-09-07T00:00:00+02:00</published>
    <updated>2026-09-07T00:00:00+02:00</updated>
    <category term="content-systems"/>
    <summary>How technical component architecture becomes product design.</summary>
    
    <content type="html">&lt;p&gt;How technical component architecture becomes product design.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.django-cms.org/resources/blog/2026/09/07/plugin-architecture-is-editor-experience/&quot;&gt;Read on django-cms.org ↗&lt;/a&gt;&lt;/p&gt;</content>
    
  </entry>
  
  
  <entry>
    <title>Make Your Django Application Editable</title>
    
    <link href="https://www.django-cms.org/resources/blog/2026/08/31/make-your-django-application-editable/" rel="alternate" type="text/html"/>
    
    <id>https://www.fabian-braun.de/writing/make-your-django-application-editable/</id>
    <published>2026-08-31T00:00:00+02:00</published>
    <updated>2026-08-31T00:00:00+02:00</updated>
    <category term="content-systems"/>
    <summary>Adding editorial capabilities without surrendering application architecture.</summary>
    
    <content type="html">&lt;p&gt;Adding editorial capabilities without surrendering application architecture.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.django-cms.org/resources/blog/2026/08/31/make-your-django-application-editable/&quot;&gt;Read on django-cms.org ↗&lt;/a&gt;&lt;/p&gt;</content>
    
  </entry>
  
  
  <entry>
    <title>The CMS Is Not the Application</title>
    
    <link href="https://www.django-cms.org/resources/blog/2026/08/24/the-cms-is-not-the-application/" rel="alternate" type="text/html"/>
    
    <id>https://www.fabian-braun.de/writing/the-cms-is-not-the-application/</id>
    <published>2026-08-24T00:00:00+02:00</published>
    <updated>2026-08-24T00:00:00+02:00</updated>
    <category term="content-systems"/>
    <summary>Where the boundary between application and content system belongs.</summary>
    
    <content type="html">&lt;p&gt;Where the boundary between application and content system belongs.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.django-cms.org/resources/blog/2026/08/24/the-cms-is-not-the-application/&quot;&gt;Read on django-cms.org ↗&lt;/a&gt;&lt;/p&gt;</content>
    
  </entry>
  
  
  <entry>
    <title>Why You Should Join the Djangonaut Space Program as a Navigator — A Veteran’s Perspective</title>
    
    <link href="https://medium.com/@fsbraun/why-you-should-join-the-djangonaut-space-program-as-a-navigator-a-veterans-perspective-3491fc78c721" rel="alternate" type="text/html"/>
    
    <id>https://www.fabian-braun.de/writing/join-djangonaut-space-program/</id>
    <published>2025-07-17T00:00:00+02:00</published>
    <updated>2025-07-17T00:00:00+02:00</updated>
    <category term="software-reuse"/>
    <summary>On turning experience into leverage: mentoring new contributors, transferring context, and strengthening the community that sustains open source.</summary>
    
    <content type="html">&lt;p&gt;On turning experience into leverage: mentoring new contributors, transferring context, and strengthening the community that sustains open source.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://medium.com/@fsbraun/why-you-should-join-the-djangonaut-space-program-as-a-navigator-a-veterans-perspective-3491fc78c721&quot;&gt;Read on medium.com ↗&lt;/a&gt;&lt;/p&gt;</content>
    
  </entry>
  
  
  <entry>
    <title>Why I Still Write Code</title>
    
    <link href="https://www.fabian-braun.de/writing/why-i-still-write-code/" rel="alternate" type="text/html"/>
    
    <id>https://www.fabian-braun.de/writing/why-i-still-write-code/</id>
    <published>2025-03-25T00:00:00+01:00</published>
    <updated>2025-03-25T00:00:00+01:00</updated>
    <category term="software-reuse"/>
    <summary>Why writing code still matters to me when building automation and systems meant to last.</summary>
    
    <content type="html" xml:base="https://www.fabian-braun.de/writing/why-i-still-write-code/">&lt;p&gt;I have been writing software for a long time, and my role has changed considerably along the way. I spend more time today thinking about architecture, product direction, automation, and the long-term consequences of technical decisions than about implementing individual features.&lt;/p&gt;

&lt;p&gt;And yet, I still write code.&lt;/p&gt;

&lt;p&gt;The reason is not that I think writing code is inherently valuable, or that everything important should be implemented by hand. Quite the opposite: I believe strongly in automation and in moving repetitive implementation work out of the way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I still write code because technical judgment requires contact with engineering reality.&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;staying-close&quot;&gt;Staying close&lt;/h2&gt;

&lt;p&gt;It is possible to understand a software system at many levels. You can understand its product, its architecture, its interfaces, its deployment, or the organization around it without knowing every detail of its implementation.&lt;/p&gt;

&lt;p&gt;That distance is often useful. Architecture requires abstraction, and technical direction requires looking beyond the problem immediately in front of you.&lt;/p&gt;

&lt;p&gt;But distance has a cost.&lt;/p&gt;

&lt;p&gt;A clean architecture diagram does not tell you where an abstraction becomes awkward in practice. A specification does not necessarily reveal which existing assumption it violates. A pull request can look perfectly reasonable in isolation while gradually moving a system in the wrong direction.&lt;/p&gt;

&lt;p&gt;Writing code occasionally takes me back into that reality.&lt;/p&gt;

&lt;p&gt;I encounter the boundary that looked cleaner from above. I discover why an old piece of code is more complicated than I expected. I find that something I wanted to generalize should perhaps remain specific, or that two apparently unrelated problems are symptoms of the same architectural weakness.&lt;/p&gt;

&lt;p&gt;Those observations influence decisions much larger than the code I happened to be writing.&lt;/p&gt;

&lt;h2 id=&quot;close-to-the-automation&quot;&gt;Close to the automation&lt;/h2&gt;

&lt;p&gt;This is particularly important to me because I like automation.&lt;/p&gt;

&lt;p&gt;Good automation removes repetitive work, makes knowledge executable, and allows people to concentrate on decisions that actually require their attention. Much of my work over the years has been about making systems do more so that people have to do less.&lt;/p&gt;

&lt;p&gt;But automation also creates distance.&lt;/p&gt;

&lt;p&gt;Once something works reliably enough, we stop thinking about what happens inside it. That is often exactly what we want. Nobody should need to understand an entire technology stack merely to complete an everyday task.&lt;/p&gt;

&lt;p&gt;The situation changes when we are responsible for the system itself.&lt;/p&gt;

&lt;p&gt;Automation encodes assumptions about processes, data, responsibilities, and exceptional cases. Those assumptions gradually become part of how an organization operates, often surviving much longer than anyone originally expected.&lt;/p&gt;

&lt;p&gt;Being close to the implementation helps me understand what we are actually automating, rather than only what we intended to automate.&lt;/p&gt;

&lt;p&gt;The goal is not to do manually what a machine could do for me.&lt;/p&gt;

&lt;p&gt;The goal is to understand enough to remain in control of what the machine does.&lt;/p&gt;

&lt;h2 id=&quot;systems-have-surprisingly-long-lives&quot;&gt;Systems have surprisingly long lives&lt;/h2&gt;

&lt;p&gt;Software has a tendency to live longer than expected.&lt;/p&gt;

&lt;p&gt;django CMS is a good example from my own work. It has evolved through many generations of Python, Django, browsers, deployment practices, contributors, organizations, and ideas about what a content management system should be.&lt;/p&gt;

&lt;p&gt;A.IX is different, but the underlying design question is similar: what does it mean to build systems that may still matter decades from now? When investing is long-term the software also needs to be long-term.&lt;/p&gt;

&lt;p&gt;Thinking in decades changes how I look at software.&lt;/p&gt;

&lt;p&gt;The interesting question is no longer whether an architecture is elegant today. It is whether the system can absorb changes we cannot yet predict, whether parts can be replaced without replacing everything, and whether somebody will still be able to understand why important decisions were made.&lt;/p&gt;

&lt;p&gt;Long-lived software does not survive by staying the same.&lt;/p&gt;

&lt;p&gt;It survives because it can change.&lt;/p&gt;

&lt;h2 id=&quot;good-design-compounds&quot;&gt;Good design compounds&lt;/h2&gt;

&lt;p&gt;This is where architecture becomes less theoretical.&lt;/p&gt;

&lt;p&gt;A clear boundary might save a little work today, but over many years it can make hundreds of changes easier. A stable interface can allow several generations of implementations to come and go behind it. A deliberately small core reduces the number of assumptions that must survive every technological cycle.&lt;/p&gt;

&lt;p&gt;Good design compounds.&lt;/p&gt;

&lt;p&gt;Bad design compounds too. A convenient shortcut becomes a dependency, the dependency becomes an assumption, and eventually that assumption appears in places nobody anticipated when the original decision was made.&lt;/p&gt;

&lt;p&gt;After enough years, the cost of a design decision has remarkably little to do with the amount of code involved.&lt;/p&gt;

&lt;p&gt;This is one reason I still want to experience some of those decisions at implementation level. Architecture is not something decided once and subsequently implemented. It emerges through many small decisions made over the lifetime of a system.&lt;/p&gt;

&lt;p&gt;You cannot steward a long-lived software system entirely from above.&lt;/p&gt;

&lt;h2 id=&quot;designing-for-replacement&quot;&gt;Designing for replacement&lt;/h2&gt;

&lt;p&gt;When building software expected to last, I don’t think the objective should be to preserve today’s implementation.&lt;/p&gt;

&lt;p&gt;The objective should be to preserve the ability to replace it.&lt;/p&gt;

&lt;p&gt;Operating systems change. Databases change. Frameworks change. Infrastructure changes. Organizations change. Requirements change. People certainly change.&lt;/p&gt;

&lt;p&gt;Trying to predict all of those changes is futile.&lt;/p&gt;

&lt;p&gt;What we can do is design boundaries that limit how far a change propagates. We can be deliberate about ownership of data. We can keep interfaces smaller than their implementations. We can avoid making the core of a system dependent on things that are likely to be temporary.&lt;/p&gt;

&lt;p&gt;A stable system is not one in which nothing moves.
It is one in which things can move without everything moving at once.
That principle matters much more to me today than choosing the supposedly perfect technology for a particular moment.&lt;/p&gt;

&lt;h2 id=&quot;digital-sovereignty&quot;&gt;Digital sovereignty&lt;/h2&gt;

&lt;p&gt;There is a broader principle behind this that I increasingly think about as digital sovereignty.&lt;/p&gt;

&lt;p&gt;The term is often discussed at the level of governments, cloud providers, regulation, or open-source policy. Those are important conversations, but there is also a very practical version of it.&lt;/p&gt;

&lt;p&gt;Can we understand the systems we depend on? Can we operate them? Can we modify them when our requirements change? Can we move them? Can we keep them running when someone else’s priorities change?
Open source helps, but access to source code alone does not create sovereignty.&lt;/p&gt;

&lt;p&gt;Capability does.&lt;/p&gt;

&lt;p&gt;An organization that technically possesses the source code to an important system but no longer has the knowledge required to understand or change it is not particularly sovereign.&lt;/p&gt;

&lt;p&gt;The same principle applies at a personal level.&lt;/p&gt;

&lt;p&gt;I don’t need to implement everything myself, and I don’t want to. But I want to retain the ability to investigate a system, understand the important parts, question its assumptions, and change it when necessary.&lt;/p&gt;

&lt;p&gt;Writing code maintains some of that capability.&lt;/p&gt;

&lt;h2 id=&quot;simplicity-creates-freedom&quot;&gt;Simplicity creates freedom&lt;/h2&gt;

&lt;p&gt;This also explains why I increasingly value simplicity.&lt;/p&gt;

&lt;p&gt;Every additional dependency, service, abstraction, platform, and protocol may solve a real problem, but it also introduces another thing we depend on and another piece of knowledge someone has to maintain.&lt;/p&gt;

&lt;p&gt;Complexity therefore has an organizational cost beyond development effort.&lt;/p&gt;

&lt;p&gt;It reduces the number of people who can understand a system. It increases the number of external decisions that can affect it. It makes replacement harder and independent operation more expensive.
Sometimes that cost is absolutely justified.
But it should be a decision rather than an accident.&lt;/p&gt;

&lt;p&gt;A system you understand is easier to change, and a system you can change is a system you have a chance of keeping.&lt;/p&gt;

&lt;h2 id=&quot;coding-is-not-the-output&quot;&gt;Coding is not the output&lt;/h2&gt;

&lt;p&gt;This changes what writing code means to me.&lt;/p&gt;

&lt;p&gt;Earlier in my career, code was much more directly the occasional output of my work. A problem needed solving, and I implemented the solution.
Today, the most valuable outcome is often a decision: identifying the actual problem, finding the right boundary, choosing what should remain stable, deciding what should be replaced, or recognizing that something should not be built at all.&lt;/p&gt;

&lt;p&gt;Writing code supports those decisions.&lt;/p&gt;

&lt;p&gt;Sometimes I implement a small part because I want to test an architectural assumption. Sometimes I need to follow a problem deeply enough to understand where it actually originates. Sometimes building something is simply the fastest way to discover whether an idea survives contact with reality.&lt;/p&gt;

&lt;p&gt;The amount of code I produce is therefore becoming less interesting to me.&lt;/p&gt;

&lt;p&gt;The quality of the judgment it enables is much more interesting.&lt;/p&gt;

&lt;h2 id=&quot;knowing-when-to-go-down-a-level&quot;&gt;Knowing when to go down a level&lt;/h2&gt;

&lt;p&gt;Moving toward architecture and technical stewardship does not mean continuously moving further away from implementation.&lt;/p&gt;

&lt;p&gt;It means being able to move between levels.&lt;/p&gt;

&lt;p&gt;Sometimes the right place to work is the product direction. Sometimes it is the architecture. Sometimes it is an interface or specification. And sometimes the uncertainty cannot be resolved from there, and the right thing to do is to go down another level and look at the code.&lt;/p&gt;

&lt;p&gt;Then you come back up with better information.&lt;/p&gt;

&lt;p&gt;The further I move from implementation as my primary job, the more deliberate I become about which code I still write.&lt;/p&gt;

&lt;p&gt;I don’t write code because writing code is the objective.&lt;/p&gt;

&lt;p&gt;I write it to stay close enough to the systems I help shape that my decisions remain grounded in how those systems actually work.&lt;/p&gt;

&lt;p&gt;For systems that should remain understandable, adaptable, and ours to change for decades, I think that matters.&lt;/p&gt;

&lt;p&gt;That is why I still write code.&lt;/p&gt;
</content>
    
  </entry>
  
</feed>
