Compass Navigation Streamline Icon: https://streamlinehq.com
applied cartography

Two hundred decisions

This week, after a little under two years of having adopted the practice, Buttondown has minted its 200th decision log. This is a practice very similar to RFCs or ADRs, but I prefer the term decision because it's both A, used for a variety of non-engineering purposes, and B, sounds a little less lame. The practice itself is extremely simple and there is no catch. It goes something like the following:

  1. Decisions are any non-trivial choice made for a specific reason.
  2. The context behind a decision is very hard to retrieve after the fact and grows in difficulty over time.
  3. The ability to revisit this context and update, invalidate, or buttress it is extremely useful in a variety of reasons.

Our decisions range from the monumental-yet-concise:

---
id: "0002"
title: What domain should we use now that we have buttondown.com?
date: 2024-05-14
status: implemented
---

We should shift to buttondown.com, but the process of doing so is very unclear and we need to take it slowly + carefully.

to the trivial-but-nuanced:

---
id: "0196"
title: "Unship django-simple-history"
author: Justin Duke
date: 2026-06-24
status: implemented
---

## Context

We carried `django-simple-history` for years to track changes to two objects: the
`Newsletter` and the `Email`. It installed mirror `Historical*` tables that copy
every field of the tracked model on every save.

It is the same kind of dependency we have spent the past year unshipping
(0148 django-safedelete, 0149
django-structlog, 0152 django-anymail): a general-purpose,
illegible, low-performance package doing work that our actual use case — narrow
and minor — does not justify. We already had a `*StatusTransition` model and
several other purpose-built ways to record the changes we cared about, so most of
what `django-simple-history` did was redundant. We held onto it mostly out of
deference to history, not because we were using it.

I have spent this month deep in the email lifecycle and now have both the
confidence that we should remove it and the loaded context to do so quickly
without coming back up to speed later. The removal is not urgent on its own — it
mainly cleans up the render and save/performance path — but the marginal cost of
doing it now, while the context is hot, is low.

## Decision

Remove `django-simple-history` and replace its two real use cases with
purpose-built models:

- Email body revisions → the home-grown `EmailHistory` model
  (`emails/models/email_history/`), one row per body change rather than a full
  per-field mirror on every save (#9795).
- Newsletter design changes → `NewsletterDesignHistory`.

We removed the package's models from Django's state but **deliberately retained
the underlying tables and their data** — `emails_historicalaccount`,
`emails_historicalemail`, and `emails_historicalnewsletter` — in case we ever
need to read the old revisions. (#9906 then dropped the orphaned `auth_user`
foreign keys those tables still carried, which were tripping a
`ForeignKeyViolation` on user deletion — BUG-387.)

## Consequences

- The save path no longer writes a full-object history row on every `Email`/
  `Newsletter` save; history is now scoped to exactly the fields we surface
  (email body, newsletter design).
- Three orphaned `Historical*` tables now live on production with no Django model
  in front of them. Like our other retired historical datasets (API requests,
  raw email events), the right end state is to archive them to ClickHouse and
  drop them from Postgres rather than leave them sitting in the primary database.
  That cleanup is tracked as a follow-up and is not blocking.

It is objectively not a free lunch in that it does take time for you to sit down and explain concisely in prose why you are doing a thing and what the other options are. It was most of all the null hypothesis of simply not acting. But a cost which serves as a forcing function to write, I mostly think of as a reward at this point.

The world is complex: with every patch of fog that lifts, I find four more in the distance. Only very recently have I started to understand knowledge as a perception of fog rather than a dispelling of it. And so I am very careful not to prescribe these days, unless I am so overwhelmingly confident in the universality of a given prescription. This is one such thing: keep a decision log.

(See also Oxide's public RFD system.)


About the Author

I'm Justin Duke — a software engineer, writer, and founder. I currently work as the CEO of Buttondown, the best way to start and grow your newsletter, and as a partner at Third South Capital.

Colophon

You can view a markdown version of this post here.