blog

How I organize Buttondown's Django applications

I nerd out about structuring a medium-sized Django project

Heads up!
This is a pretty technical post with absolutely no new information about Buttondown's features! If you're not interested in Django (or don't know what Django is), feel free to skip this one.

Wait, what's Buttondown?

I run Buttondown, a newsletter tool. It's a pretty nice tool, in my opinion! Please check it out if you get a chance.

This application is around 45,000 lines of Django code [^2], with commits dating back to 2016. It's grown and mutated a lot over the past six years.

I've been meaning to write more about how my approach to structuring and developing Buttondown has changed over the years, and I think a good jumping off point would be the biggest conceptual tool Django offers to organize your code: apps.

Wait, what are apps?

The official Django docs define applications as:

A Django application is a Python package that is specifically intended for use in a Django project. An application may use common Django conventions, such as having models, tests, urls, and views submodules.

Okay, that's not particularly useful. Two Scoops of Django is more useful:

Django apps are small libraries designed to represent a single aspect of a project. A Django project is made up of many Django apps. Some of those apps are internal to the project and will never be reused; others are third-party Django packages.

Or to go further still, to quote James Bennett:

The art of creating and maintaining a good Django app is that it should follow the truncated Unix philosophy according to Douglas McIlroy: ‘Write programs that do one thing and do it well.”’

Apps are essentially a DSL around modules: they provide a level of namespacing and structure around logically disjoint pieces of functionality & business logic.

My general philosophy with apps design

As far as I can tell, there are two main competing visions for how to use apps in Django:

  1. Apps should be self-contained groups of primitives (models & business logic) that are logically independent, as quoted above.
  2. Apps are a dumb idea for the vast majority of codebases and introduce a bunch of unnecessary friction, to be avoided at all costs. [^1]

I think I land somewhere in the middle, but leaning towards the former camp. My heuristic is roughly: is this code something that I would want to break out into its own codebase or deploy target at some point?

Here's a top level overview of the apps Buttondown has with some high-level metadata; keep scrolling for more in-depth information.

| app | lines of code | number of models | year introduced | | ------------------------ | ------------- | ---------------- | --------------- | | api | 2000 | 1 | 2018 | | checker | 1300 | 4 | 2021 | | email_address_validation | 1500 | 3 | 2018 | | emails | 30000 | 37 | 2016 | | events | 1400 | 1 | 2017 | | flags | 500 | 1 | 2021 | | marketing | 500 | 2 | 2016 | | markdown_rendering | 1000 | 5 | 2019 | | monetization | 3500 | 11 | 2020 |

api

Buttondown has an external-facing API with the long-term goal of exposing all important parts of the app's functionality for programmatic access. (The somewhat silly Platonic ideal I strive towards: someone should be able to build out their own Buttondown client just with the API surfaces I provide.)

As such, I carve out that API as its own app. This app doesn't own many models in of itself — except a fairly generic APIRequest model which tracks incoming requests and outgoing responses — but acts as a superstructure for various primitives owned by emails, owns the schema validation & DRF views, that sort of thing.

If I had a magic wand and/or infinite time, this app would probably be shaped a little differently: more like a series of middlewares and decorators that, when applied to other apps, could expose those apps to the external API.

This is a nice theoretical exercise, but the ROI on such work feels very low. I know apps are mostly about model separation, but I think "separation of concerns" (mushy of a phrase as that might be ) is particularly valid, and has served me well here.

checkers

One of the internal frameworks I really loved at Stripe was "checker", which was a very pleasant DSL for declaring programmatic scheduled invariant checks in your code. This is my shamelessly reappropriated version of that framework, and its proven so invaluable I'm surprised that it's not more ubiquitous.

The core of this app is a decorator, @register_checker, which takes a function that returns CheckerFailure objects and does a bunch of metaprogramming to email (or page) me whenever a list returns something.

Sometimes I use this for administrative tasks, like manually auditing new accounts who connect Stripe accounts with pre-existing customers & charges:

@register_checker
def no_stripe_accounts_need_auditing() -> Iterable[CheckerFailure]:
    for newsletter in Newsletter.objects.filter(
        paid_subscriptions_status=Newsletter.PaidSubscriptionsStatus.NEEDS_AUDITING
    ):
        if newsletter.stripe_account:
            yield CheckerFailure(
                text=f"Stripe account {newsletter.stripe_account.account_id} needs auditing",
                subtext=f"""
                    Newsletter: {newsletter}
                    Link to Stripe: https://dashboard.stripe.com/connect/accounts/{newsletter.stripe_account.account_id}
                    Admin URL: https://admin.buttondown.email/admin/emails/newsletter/{newsletter.id}/change/
                """,
                data={"newsletter_id": str(newsletter.id)},
            )

Other times, I use it for checking to ensure that no emails are in a problematic state space that necessitate re-driving or SMTP shenanigans:

@register_checker(
    severity=Checker.Severity.HIGH, cadence=Checker.Cadence.EVERY_TEN_MINUTES
)
def no_emails_stuck_in_flight() -> Iterable[CheckerFailure]:
    for email in fetch_relevant_emails():
        text = f"Email {email.id} (from {email.newsletter.username}) is stuck in flight"

        expected_receipts = calculate_expected_receipts(email)
        if not expected_receipts:
            continue

        actual_receipts = EmailDeliveryReceipt.objects.filter(email=email)
        # 'high' is the queue used for asynchronously_send_email_to_recipients.
        queue = django_rq.get_queue("high")
        current_backlog_size = queue.count

        subtext = (
            f"Expected {expected_receipts.count()} receipts "
            f"but only received {actual_receipts.count()}.  "
            f"Current backlog: {current_backlog_size}"
        )
        yield CheckerFailure(
            text=text, subtext=subtext, data={"email_id": str(email.id)}
        )

(I really, really want to open-source this, and probably will at some point this year.)

email_address_validation

The email_address_validation app acts as a simple interface that takes strings and returns a ValidationResult object associated with them. That result is compiled from a number of heuristics, from regular expressions to internal data to external services like CleanTalk and Mailgun.

I'm pretty happy with this interface! I originally came to the approach with the concept of one-day deploying this as a stand-alone SaaS, and while my appetite in doing so has largely abated (feels like a lot of boilerplate for a fairly small incremental bump in revenue) it's made the rats' nest of logic well-encapsulated.

emails

emails is, as one might guess from the name, the oldest and largest app in the codebase. It contains the three most important models in Buttondown — Subscribers, Newsletters, and Emails — plus another 34 to boot. By default, most logic ends up here; it is the sun of Buttondown's little heliocentric universe, as you might have surmised from that summary table above.

I've idly mused on what splitting up emails would look like, and haven't come up with any satisfactory answers. The logic here is tightly coupled, and decoupling feels very low-ROI.

events

Buttondown generates a lot of events from third-party applications. I store data points for every attempted (and successful) email delivery in order to both surface analytics to newsletter authors and to track internal heuristics like particularly spammy authors or slow-to-respond domains. That's not even getting into opt-in functionality like click or open tracking which leads to even more events.

Plus, Buttondown connects to a swath of ESPs (Mailgun, AWS, and Postmark to name a few) which means the interface for events between providers has to be relatively consistent. Enter this app, which stands up a bunch of webhook routes and munges responses into a generic EmailEvent object that contains pointers to other models.

(Also, shout out to django-anymail, which makes this process much easier.)

A not so fun fact: I've titled this section events, but the name of the app is actually mailgun_events — Mailgun was Buttondown's first ESP, so it made sense at the time. Mailgun's now only processing around 5% of Buttondown's email traffic. Let this be a lesson to you all!

flags

For a decently long amount of time, I used django-waffle to manage flags and switches for Buttondown. I try to avoid phased rollouts in the "1% of traffic, then 10% of traffic, then 25% of traffic" vein except for very fraught situations, but I find these primitives useful in two scenarios:

  1. Pieces of functionality like "make sure this specific newsletter returns a 301 instead of a 300 because I signed a lucrative annual contract with them" which I really don't want to enshrine at the database level
  2. Circuit breakers like "turn off all access to CleanTalk because they started returning a bunch of timeouts and I don't want it gumming up the works of synchronous API requests."

I decided to end up ditching django-waffle and focus on a lighter-weight flags system to reduce my third-party dependency surface area a bit and, more importantly, to reduce the number of database queries I'd need for some particularly chokepoint-y areas like email rendering or address validation.

I'd like to open source this app at some point: it's very lightweight and conceptually simple.

marketing

If I could really wave a magic wand, I'd have this app not exist at all: conventional wisdom these days is to have your marketing site be in a different codebase entirely, and to have your core application be on app.domain.com.

Unfortunately, I was not conventionally wise in 2016, and at this point it's not so trivial for me, since buttondown.email/<username> is where most folks' archives live and I'd need to do some tricky routing shenanigans.

So what do I do instead? I serve the marketing pages in boring ol' Django, and store the code for them in a standalone app so the views and routes are at least in their own little world. There are a couple models here — namely Updates (which powers a little widget in the login page letting folks know about new features) — but data and business logic is thin-to-non-existent here.

markdown_rendering

Buttondown is a huge fan of Markdown, and has quite a bit of dedicated logic to make it play nice: I've got custom extensions for tables, footnotes, highlighting, smart embeds (for things like Instagram and YouTube, whose oembeds are not email-friendly due to their use of iframes), and much more.

This all culminates in a single method meant to be the external 'interface': render, which takes a string and a RenderingTarget (either "for the web" or "for email").

This has been a really nice encapsulation of business logic & function. While I don't see myself open-sourcing or spinning off a dedicated rendering instance or anything like that, it makes it very easy to dive into rendering esoterica and it leads to very maintainable code.

monetization

This app is a bit of a misnomer, since it sounds like it might pertain to either managing paid newsletters or managing paying users. It's actually even more simple than that: it's a series of webhooks and models meant explicitly to replicate Stripe's state space in my own database.

This app explicitly doesn't have any business logic; any operations like "mark a subscriber as churned once a StripeSubscription is updated" or "create a new subscriber when a new StripeCustomer is created" are handled within emails.

(I'm aware of the existence of dj-stripe, which seemed...a little cumbersome and confusing the two times I tried to onboard to it. Building out my own equivalent probably took more time than it was worth, but I appreciate the gradually revealed complexity and it felt like a particularly important part of the database to treat with care.)

Linus asked to hear more about why do this at all, let alone with a bespoke app. A couple reasons, in descending order of magnitude:

  1. Complexity. Buttondown cares about Stripe a great deal more than the median SaaS app. While its billing model is fairly simple, it also uses Stripe Connect to manage paid subscriptions for hundreds of other newsletters, meaning that there are a lot of subscriptions, plans, customers, and so on.
  2. Speed. Not only does Buttondown care about Stripe a great deal, Stripe is often in the critical path of customer-facing transactions. Billing flows in general can afford to be slow: it doesn’t really matter if it takes ten seconds to generate an invoice view that’s hit by some back-of-office administrators a couple times a day. However, it’s important that Buttondown can answer questions like “when did this paid subscriber pause their subscription?” or “how many new customers did this newsletter get last week?” relatively expediently, because these are core flows! And where core flows are concerned, network calls to api.stripe.com are painful, let alone having to deal with some of the pagination and eventual consistency tradeoffs Stripe has enshrined.
  3. Connect. Certain queries I want to keep track of from an operational perspective are literally impossible in Stripe at the moment, such as “give me all disputes across all connected accounts in the past 24 hours.”

In terms of how I actually model this within Postgres: I try to respect foreign key relationships, and plop the rest of the data in JSONB. For example, here’s the StripeSubscription model:

class StripeSubscription(BaseStripeModel):
    # Core information.
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    data = models.JSONField(default=dict)

    # Relevant dates.
    creation_date = models.DateTimeField(auto_now_add=True)
    modification_date = models.DateTimeField(auto_now=True)

    # Stripe-side primary key.
    subscription_id = models.CharField(max_length=100, unique=True)

    # Foreign keys.
    customer = models.ForeignKey(
        "StripeCustomer",
        on_delete=models.CASCADE,
        related_name="subscriptions",
    )
    account = models.ForeignKey(
        "StripeAccount",
        on_delete=models.CASCADE,
        related_name="subscriptions",
    )
    plan = models.ForeignKey(
        "StripePlan",
        on_delete=models.CASCADE,
        related_name="subscriptions",
    )

Conclusion

That's the full list! I subscribe to a pattern that I suspect is fairly common amongst codebases of Buttondown's size: one overstuffed, gross "primary" application and a bunch of orbiting, comparatively better-designed ones.

I like using apps: they make my brain happy in the same way cleaning up test code makes me happy (which is to say, sometimes the happiness is worth the lack of obvious business value.) That being said, I'd be warier of being over-aggressive with app usage as opposed to being over-conservative; I've lost afternoons to chasing down cross-app migration issues whereas besides some very long models lists I can't point to any specific footguns I've stumbled upon from having a Very Big Django App.

Please let me know if you have any questions — I'm all ears!

[^1]: As someone who's lost five hours of their life to shenanigans with cross-app migration squashing, I can certainly sympathize with some of the histrionics of this world-view. [^2]: Well, and a bunch of front-end code, but that's out of scope for this post. Also, my front-end code is much more poorly organized than my back-end code.

What customers say about Buttondown

I made the transition from MailerLite and I have no regrets. I also like that Buttondown focuses on the essentials by design and keeps me grounded and centered on what really matters.
Arthur Cendrier
Author, Thoughtful Inquiry
Overall, Buttondown has been terrific to work with and I recommend them for anybody who's thinking of starting a newsletter or moving over like I did.
Andy Magnusson
Customer Engineering Leader
Wanna know how good Buttondown as a product experience is? I upgraded to Basic before sending the first email, and then upgraded again two days later.
Zak El Fassi
Founder, Zaigood Labs
Mailchimp lost me due to their inferior product and the nightmarish merry-go-around experience with their overseas support team. Buttondown won me over with their superior product and second to none customer service.
Sav Tripodi
CEO, Sanico Software
Your support is amazing and I deeply appreciate how available and helpful you are. I LOVE being able to turn tracking pixels off. I didn't even realize this was an option when I signed up and am SO HAPPY to not track people.
Andrea Mignolo
Method & Matter
I'm also impressed with how responsive you are, and how you directly answer customers. Makes it really clear that signing up for your service was a good decision.
Nicole Tietz-Sokolskaya
Software engineer and writer
Very happy with Buttondown, works smoothly, it's very configurable and I love the minimalist design of the UI. It makes me focus on my writing. Plus, I'm super happy to support independent software and I should mention - the support I receive whenever I have a question is warm and quick :)
Martina Pugliese
Data scientist and storyteller
I just tested the RSS to Email feature for one of my blogs and it was incredibly easy to set up. It took me about 30 mins to figure out the same feature in Mailchimp.
Nicolas Bernadowitsch
Blogger
This long weekend I fulfilled a long-standing promise to myself to switch my RSS-to-email provider from Mailchimp to Buttondown, and it’s been such a great experience. It’s cheaper, more flexible, less cluttered, and it’s run by Justin Duke who is just delightful and answered a bunch of my questions over the weekend (even though I asked him to please not!).
Rian van der Merwe
Director of Product at PagerDuty
I've been wrangling half a dozen tools to get my stuff up and running recently, almost all of which had some hiccup. Buttondown had zero. It did everything I expected and needed the first time.
Catherine Cusick
Self-Employed FAQ
I, like almost everyone else I've seen talk about Buttondown, am IMMENSELY happy and impressed with your customer service. It turns out we can have nice things, which is really refreshing.
Ed Yong
Staff writer, The Atlantic
Email makes the world go ‘round, and Buttondown is how I manage it all for my keyboard projects.
Tim van Damme
Founder, MVKB
It's a truth, that should be more universally acknowledged, that Buttondown is the best newsletter software. Simple, does exactly what it sets out to do, and reasonably priced.
Noel Welsh
Founder, Inner Product
Buttondown is the perfect fit for my headless newsletter use case. And I contacted support with some specific requests and Justin responded within 30 minutes with great answers and a nice pinch of charm.
Sam Roberts
Software engineer, Tamr
Hands down the easiest way to run a newsletter - and the free version is generous!
Javeed Khatree
SEO expert
With API and Markdown support, you can build workflows that make it so easy to write.
Westley Winks
Peace Corps
I’ve never enjoyed writing newsletters as much as I do with Buttondown.
Kevin Lewis
You Got This!
Buttondown remains the easiest thing I use regularly, and I am grateful for that.
Casey
Journalist
It's a humble app doing a common job but with end users in mind.
Si Jobling
Engineering Manager
Buttondown has been an amazing experience for me. The service is constantly being improved and customer service is the best. My newsletter with Buttondown has grown from a fairly small list to over 15,000 subscribers, and it hasn't broken a sweat yet.
Cassidy Williams
CTO, Contenda
I switched over to Buttondown from Mailchimp because of the difficulty I had with Mailchimp's campaigns, so Buttondown's easy and user-friendly system has been a genuine breath of fresh air.
Jessi Eoin
Illustrator + Comic Artist
You’ve truly built a great product that I feel good about using (vs a monopoly from our tech overlords).
Rachel
2030 Camp
I love how personal Buttondown feels, especially compared to Mailchimp, Convertkit, and services like that.
Simen Strøm Braaten
Designer
This product has been exactly what I’ve needed!
Nathan Bird
Podcast host, Chattanooga Civics
It's already so refreshing compared to the mega companies.
Casey Watts
Author, Debugging Your Brain
Definitely will be using for the foreseeable future. It’s a great service and I feel well cared for. Thank you!
Phoebe Sinclair
Author
I’m a sucker for elegant UI and I really love your site, but above that I think your product has so much value for so many different people. I’m not a coder, I’m only familiar with the bare basics, but I was able to figure out and utilise Buttondown quickly.
Claudia Nathan
Founder, The Repository
The killer feature for me: Buttondown will take an RSS feed then automatically slurp up the content (in their words) and then send it to our subscribers. Job done. They seem like a good company too, so I’d say this is a winner.
Andy Bell
Founder, Set Studio
As a recent user of Buttondown, they are super on the ball. A week ago I discovered a security vulnerability and reported it on Friday afternoon. They acknowledged and fixed it in under two hours. On a Friday night! Talk about going above and beyond for your users!
Predrag Gruevski
Principal Engineer, Kensho
Well may I just say your support experience is already approximately 1 billion times better than ConvertKit. Excited to be switching!
Michael J. Metts
Author, Writing is Designing
Privacy focused sending and sign up form; lets me focus on writing - editor is "just" markdown; simple, elegant design template looks like a blog post; the founder is amazing - he's helped with every question I've had, even outside of Buttondown.
Joe Masilotti
Founder, RailsDevs
We need more nice and professional services like yours on the web.
Tobias Horvath
Designer and developer
No one is paying me to say this, but I love @buttondown so far for my lil newsletter. It’s so smart, simple, and attractive (and to my knowledge, not actively anti-trans!). Customer service is also legitimately excellent.
Julie Kliegman
Copy chief, Sports Illustrated
I love it! It lets me breathe, not compete as I write with other writers.
Devin Kate Pope
Writer and editor
It’s a pleasure working with you. Thank you! (And what a contrast with Mailchimp, where I spent two weeks and a dozen of emails trying find out why our form goes down sometimes (only sometimes), and never really got a real answer.)
Anton Sotkov
Software Engineer, IA
Buttondown exemplifies how I wish most software worked, and I hope to achieve a similar thing with the software I develop in the future.
Matt Favero
Software engineer
It feels incomparably good to be able to email just like a guy named Justin when you have a @buttondown question 15 minutes before you’re about to blast a Geistlist email. (Not a guarantee but wow this guy is human-level good.)
Jacob Ford
Designer About Town
Enter Buttondown, Justin Duke’s lovely little newsletter tool. It’s small, elegant, and integrates well. And it is also eminently affordable.
Will Buckingham
Author
Your settings page is a joy to use and everything about Buttondown makes me happy.
Gareth Jelley
Magazine editor
have been on Buttondown for ~18 months and I can't recommend it enough.
Elizabeth Minkel
Podcast host
You really do make ALL other customer service look terrible by comparison.
Chris Mead
Improv teacher
There is a caring person on the other side of this software, which is one of the things I like the most about Buttondown.
Keith Calder
Film & TV Producer
I’d also like to add that @buttondown is an absolute joy to use. Hats off, Justin!
Elliot Jay Stocks
Creative Director, Google Fonts
Shoutout to @buttondown and @jmduke for building an amazing bootstrapped product for newsletters, all while being very open to feedback and connecting directly with customers 🙏 Easily one of the most enjoyable product experiences I've had.
Den Delimarsky
Head of Ecosystem, Netlify
if you are looking for "newsletter tool for hackers" i tentatively believe the answer is @buttondown full api, compose in markdown, good docs for setting up domain auth, simple subscribe form HTML that you style yourself (or not)
Brian David Hall
Author, Your Website Sucks
I really like @buttondown as a blogging platform, it has the simplicity of Substack but the corporate culture is less toxic.
Chad Loder
Extremism researcher
I worked with @buttondown and asked for some new payment support beyond the supporter single tier / pay-what-you-want options. Justin was great and built it in just a couple days.
Dan Hon
Author & consultant
I write nonfiction and I use @buttondown buttondown.email/Changeset - indie, GREAT personal customer support, very nice default styling, all the options I want including ones to protect my readers' privacy
Sumana Harihareswara
Open source maintainer
I use @buttondown because it does exactly what I need (manage subscribers and send markdown emails), not more and not less 👍 As a bonus it's made by an indie dev which I love!
Max Stober
Founder, GraphCDN
If you’re considering running an email newsletter, or if you already run one and are considering a change of provider, I highly recommend @buttondownemail. Super-easy app, very fair pricing with a generous free tier, and exemplary support. 💯
Peter Gasston
Technologist and speaker
imo @buttondown is easily one of the best-designed services i’ve used in recent years, if you have a substack you should really consider switching!
Kabir Goel
Engineer, Cal
Thanks for getting me excited about email newsletters again.
Garrick van Buren
I'm very thrilled that I can just write in Markdown without having to deal with email builders and all that crap.
Parham Doustdar
Thanks again for all the help! You’ve really turned something super complex into something super easy – sending new issues is as simple as firing off a text message.
Kartik Chaturvedi
Thanks for creating a simple way for people who want to, like, put words in a hole and have it sent to people... I am just thankful that something just nice and human exists on the internet.
Emmanuel Quartey
I tried 3 other newsletter services today and I felt like wanting to rip my hair out. They were all painfully slow. I'm so glad I found Buttondown.
Mohamed Elbadwihi
I’ve found Buttondown to be a great fit for my workflow and have been delighted by all of Justin’s thoughtful features and improvements to the product.
Michael Lee
Like seriously, so many lovely little easter eggs in one could-be-boring service.
Alexandra Muck
I just switched over from Tinyletter and I'm really excited to have found a place to host my tiny newsletter that doesn't seem like it's assuming everyone sending newsletters is an email marketer / growth hacker.
Tessa Alexanian
I'm in love with the simplicity of Buttondown.
Ekfan
I’ve used similar tools in the past and Buttondown is by far the simplest to use and most promising.
Fabrizio Rinaldi
Thank you for creating such a simple and brilliant tool. I’ve just signed up and the experience has been smooth and painless (the docs are great too!)
Oliver Holms
As a developer who has hated every email system I've ever used this is so nice.
Drew Hornbein
I wish I still wrote a newsletter just so I could use buttondown again. It’s like that.
Steven Kornweiss

Ready for a better newsletter?

See why thousands of businesses and creators use Buttondown to send millions of emails every day.

Best practices, news, and more. Every month.

See why thousands of businesses and creators use Buttondown to send millions of emails every day.

Have a question? We’re here to help.

We're email experts who are always available to answer your questions.