Free Threaded Django with Micah Lyle
Published October 23, 2025
This video features Micah Lyle at DjangoCon US 2024 in Durham, North Carolina, USA.
In this talk, I’m going to demonstrate a new perspective on how to structure Django codebases using something I call “Operations”, so that Django codebases can scale well with any amount of features or complexity inside an application as it grows over time.
This talk was presented at: https://2024.djangocon.us/talks/operations-missing-django-piece/
LINKS:
Follow Micah Lyle 👇
On GitHub: https://github.com/MicahLyle
On X: https://x.com/micah_lyle
Website: https://elyon.tech
Follow DjangoCon US 👇
https://fosstodon.org/@djangocon
https://x.com/djangocon
Follow DEFNA 👇
https://www.defna.org/
Video production by Confreaks
Follow Confreaks 👇
https://confreaks.com
https://x.com/confreaks
Django applications often begin with straightforward models, views, and templates, but business rules can make model methods and views grow into hard-to-follow “God objects.” Micah Lyle proposes an operations layer: business actions that live in an `ops` package, sit between entry points such as views or tasks and the models, and coordinate interactions across the system. Operations provide a searchable map of the application’s core behavior while keeping input validation, authentication, and model-specific state changes in their appropriate places.
Summarised automatically from the transcript.
Automatically transcribed, so expect mistakes in names and technical terms.
Hi. Thanks for coming out for the last talk of the day. I know there's been a lot of great talks today. And full disclosure here, I am the backup speaker, and I'm also first-time speaker. And so, you know, I think they were hoping to avoid having to bring me in, but then unfortunately someone got sick. And so here I am. So please bear with me as this is the first time I've done this at a Django Con or any tech conference. The title of my talk is Operations: The Missing Django Piece. And what I'm gonna do is talk about an abstraction layer that I think can be very useful to include in your projects as your Django code
base evolves and gets bigger over time. So bear with me for starters. I'm going to talk about the backstory, and then we're going to go into the Django model view template system and then why it was structured that way, what that means, what are the consequences of that. And then we're going to talk about what to do as business logic evolves over time and things get more complex, and talk about ways we can tame that complexity and structure our business logic. As our apps evolve. And then if we have time, I'll do a tiny bit of a case study, which is just going to be looking at some operations in practice. Alright, so Born and raised in San Diego, a little bit about me. I love beach volleyball, skiing, and power metal.
And so that's me right before I went to a Dragon Force concert, who is my favorite band, unashamedly so And it's funny because Django is named after Reinhardt Django, who's a gypsy jazz guitarist. And one of my dreams, maybe goals for 2026. is uh to create a software library called Django Force. I have no idea what's going to be in there yet, but that will be decided later. And so I've been a Django freelancer for several years. And as a part of that, I've built projects from the ground up, dozens of them, and I've also come in and maintained existing large systems. And so I want to acknowledge that the opinions I'm going to express in this presentation
come from the environments that I've been in. I have not taken just one Django app and maintained it for 10 years like some people I know have and they've learned different things than I've learned. But I've got the breadth of having done lots of this, these apps over and over and over and over again And these are some of the things that I've found are helpful. And so before I continue on , fun story about me is I, with my wife, we run a company called Elion Technologies. And so for all the uh Git Merge fans in the room here, that's basically what happened to my life a few years ago. is I was a freelancer, I was happily just coding away on Python Django things. And my wife was doing her UIUX thing and we thought, well, why don't we get together and start a company?
Because I'm terrible at design and you can't code. So that's what happened. And now we have a company. And then for any fans of Git Rebase in the room, if you're anti -merge and pro rebase. That's what happened next is we have a full team now, and Melissa who's here, and Alex joined the team, and there are four of us, and yeah. We have a niche as a company. We focus specifically on Django and React. Please don't ask me to do HTMX. I'm not very good at it. And I have high respect for those of you that do. We focus on apps where uh React is usually fits the use case pretty well. And so anyways Let's get into the core and the meat of this.
Uh, what I'm going to talk through is how does a Django app evolve over time typically on the projects I've worked on and the projects I've seen. And for starters, It starts out pretty simple, right? When you have an initial Django app or you're going through the tutorial with polls and all of that, you start out with a model or two. And maybe some views. And then you start out with a simple Django template with maybe some JavaScript sprinkled in. So I pulled in the classic example from the internet called to-do MVC. Which is where they implement this to-do app in like 20 different languages and 47 different frameworks. And that's how you can quickly see like how does one framework do something versus another. And so, you know, if you've worked in Django
at any time, you could probably take a look at that UI and those designs. And Have a model in your head of how you would do that and have a view in your head of how we would do that. And uh it might look something like this. And hopefully that shows up okay there. But you know, you have this to-do model, and for fun, I decided to name a field what? What am I doing? And uh You know, you might have this, is it completed? Is it cleared? Maybe you want to be able to just clear it. And uh maybe some timestamps for when it was created and when it was completed. And uh some sort of status which I actually didn't put a field there. Backup speaker. And um Anyways, so that's that's where you start.
And then what happens in a lot of my projects, and since I do a lot of service work and a lot of agency work, you know, I have some say into what the client wants to do, but ultimately it's what they want to do. What happens is you end up you start as a to-do app and you end up in this really complicated task management app that does 500 different things. And I'm using a picture here of ClickUp, which is one of many, many uh task management management apps. My company happens to use them. Uh I'm not advocating for or against them. I am just using them as an example of they literally try to do everything. And what starts as a keeping track of things to do turns into you know dates, recurring dates, uh automation
rules, multiple assignees, the idea of a task unblocking another task. And this idea of marking a to-do as complete becomes, you know, 500,000 lines of code. I'm exaggerating a little bit, but you'll get the point here. Um please don't read this code as like trying to figure out what it's doing because it's entirely made up. The point of this code is to show, and I don't even know how visible that is. uh that complete to do function, which might be two lines of code, can quickly turn into a gigantic mess of things which set sprint velocity and transition the status and unblock waiting tasks and uh reconcile wormhole collisions, evidently. Uh ChatGPT wrote this for me.
So I was asking it for an example of something that looks complicated to streamline things, as I was the backup speaker and did not have a ton of time to prep this part. So anyways. This is where things go. And while this might not be exactly what your app looks like, I'm sure many of you in here have felt the pain points of this one method or function that you wrote two years ago that was 10 lines of code. You come and look at it today and you're like, when did this become 200 lines of code? And so going from here, um, before I proceed, I want to actually take a step back. Into why do we structure Django code the way we do, or how does it start in that place and how does it tend to evolve?
Well, Remember that Django is a model view template framework. That's what MVT stands for. And Django was designed and built uh about 20 years ago and this is a great design pattern for the type of app it was initially built for and so Django expects you to define models which represent your database state and all the things you're gonna want to store, you're gonna wanna retrieve, and you're gonna want to present to your user, the things they're gonna need to store, the things you need to hold in your database. And it provides an amazing abstraction called a model for you to do that. And I still think Django models are the best in the business. I've tried lots of other ones and I just, their models are so nice. And so Django also provides views.
You know, they're like, well, you're gonna need to be able to interact with that data, so we will give you entry points into the application where you can get it, where you can update it. where you can change it, maybe where you can delete it. And so Django exposes views for that to happen. But those views are allowing computers to interact with your application via like an HTTP protocol And that's not very human-friendly. And so ultimately we need to render something that the user can use, which is a template. And in templates, you can put all of your UI presentation and display logic. And with those three things, you can go incredibly far. You have all your data in your database defined for any any actions anybody needs to take on your application. You have views.
So that can handle the logic that pulls your models in from the database. And then you have your template, which can, in this example, loop through those things you've pulled in from your database and retrieved in your view and put in your template context. And you render it to the user. And that is the ideal world, and that is the world that Django starts you out in. So then it gets complicated. I love the ideal world. I wish it just stayed there. I wish that our views were clean and it's just easy. We just pulled things in from the database. But unfortunately, at least in the business world I live in, there's something called business logic. Which, for whatever reason, is never straightforward. It's never this thing of, oh, can we just have a list of to-dos?
It's like, well Except for if they're completing the to-do on a Sunday, I want you to transition it two days to the next due date. Instead of on a Monday, I want it to be one day. And we are uh my company is inundated with requests like this all the time. And so code naturally gets a little bit messier over time. And it also gets more complex. And so, you know, using the funny, you know, how it started, how it's going, uh meme or whatever you want to call it, we start with something like this where we have a simple view called mark to do complete. And yes, this would be a post. I'm missing a decorator there and other things like that. But the act of marking a to-do complete starts with a model method. Called mark complete, which is great.
It's straightforward. The to-do is manipulating its own state. It's setting is completed to true and it's setting completed at to the current timestamp, and then we're saving it. And we've marked that to do as complete, right? Well, how it's going. Back to this diagram And our method that just marks things as complete, and please don't try to, you don't have to try and read this, is now doing a whole lot of stuff. It's opening a transaction, setting all the active blockers to completed, grabbing project history, updating that street, unblocking the blocked tasks, and our favorite, notifying the users. And it's very funny, uh, you know, especially at systems like this where there's tons of notifications, that's gotta get thrown in somewhere.
And yeah. It's probably doing a lot more as time goes on. And so our method on the to-do model that was marking this to do is complete. is now talking to like four other models, calling all sorts of who knows what functions and and and code in other places, but it's still sitting on this to-do model. And what you find is that this method is now diverging far from the original intent. And so if you run into this in your own systems, you might find it's like, well, it feels like there should be something more clear, but but what is that? And so we're gonna talk about that. And you know, as I mentioned, this hard to maintain Hard to follow what's going on, and then hard to retain the
original encapsulation of to-do, where it's becoming a God object that knows everything about all the other things in your code base. And so what I'm gonna now introduce is how I choose to solve that problem at scale. And this is my opinion, this is what has worked for me, and this is what has worked for my company. Um we've implemented this on probably a dozen Django projects at this point. And it's something that I call an operation. And that is a very generic term, so I'm gonna break down more specifically what an operation means in this context, and we're gonna go from there. So what I'm gonna say is, hey, if you want to keep that mark completed method on the to-do uh model.
Then great, keep it there, but let mark completed be responsible for manipulating the model fields on to-do or the thing that's immediately there that's that's obvious. And as soon as we start to interact with other models or other parts of the system, I want to consider adding a layer into the software to facilitate those interactions. I call that layer operations. And unlike things where you've done you've probably heard of terms like services, and maybe you use those Or maybe you've heard things in the world of domain-driven design, you have aggregates and you have all sorts of units of work, you have all this stuff. If you want to go that route, great. This is one opinionated way to go. That route, you have to learn a bunch of functionality and terminology, and it 's
it's more complicated. This is actually very simple. An operation can be whatever you want it to be. It could be a class, it could be a function, a method, a decorator, it could be a context manager. It's there's not a lot of restrictions on what it can be. And my only requirement for an operation is that it lives within an ops folder or an ops. py file. Very similar to how, and let me see, I might actually be able to fast forward. Very similar to how in a Django project, as it grows , You might start with models. py, but when you get to a bunch of models, you might start splitting them into a models folder where the init is just importing those models. And operations similarly just live within an ops
folder. Or operations, if you want to spell it out, or you could use a different name for it in your system. But in this ops folder, you are dealing with the things that actually happen in your application. You're dealing with the business logic. Notice that we might have a model here for this account system for doing invitations. And yes, we have operations for invitations as well. But we have operations for data consistency that aren't tied to any of the models. We have operations for uploaded images as well that aren't just tied to one model. And so they're in their own file. They have their own stuff going on. And there are four things that make something an operation. So one is that it's basically any sort of Python object or thing. There's probably more things you could add here than I had listed.
Two, we already talked about is that it sits within an ops folder or it's nested deeply within some ops folder. As soon as it's within the ops folder, it is an operation. And three, It does not start with an underscore. We're going to talk about this in a second. And why I'm going to actually introduce the concept of, you know, in other languages you'll hear of private methods or things like that. And then four, it actually has to do something. So operations might return a result that you might make a data class, and I'll show that in a little bit later. And because they return that result, that thing is not necessarily an operation, it's just a Uh a data class, or if you liked making type dictionaries to represent return values, um
sometimes you you know you have those and those wouldn't be an operation because they're not doing anything, they're just returned, they're just holding data. And so Typically, you only make something an operation if there's really two major conditions. One, non-operation code needs to call it. So the views call it, the management commands call it, or tasks if you're using Celery. Now this is a bit of an opar oversimplification. What you find as your app grows is that when you start, you simply have views. py. You're rendering some HTML But if anybody's had to do work in the background where, oh well, we need to check these once a week and trigger these records by marking them as expired, you're now calling business logic from outside of a view.
You're calling it from a task. And so what if your business logic needs to be used in a task and a view? Does the task call the view? No, because that doesn't make sense. So your business logic shouldn't live in the view. So it's got to live somewhere else. And that's why I part one of the many reasons we come up with the operations abstraction. And so, you know, some outside code or some entry point code into your application. If it needs to call it to make things happen, then you should make it an operation. Or maybe there's some aspect of in this made-up scenario system. of invitations. Maybe when an invitation is accepted, it needs an account needs to be created for the person or they need to be associated with the account. Well, then we have an invitation that uh an operation
sorry that accepts that invitation and that operation calls the one that attaches it to the account or or something that's more specific to accounts And so operations can also call out other operations. And you don't make something an operation. If it's a dependency of an operation such that it shouldn't be called on its own. Right? Like we all want to write clean, reusable code and Sometimes we need a helper method or we need a helper function. And if you could imagine something like a sign-up operation. On a modern web app, when you sign up, usually it doesn't just create one model. You might create a user, you might create a login record, you might create a signup record that forwards those signups to some sort of like
uh notification system so a founder could figure out oh five people signed up today right and if you wanted to do something like create a signup record you should never be doing that on your own on its own. You should be doing that as a part of the signup operation that is responsible for signing the user up. And so we'll show a better, it'll but that's better seen than than talked about, and so we'll talk about that in this uh in the example. But uh you also don't make something an operation if it genuinely belongs somewhere else. And this is not something we can fit in a 25-minute talk other than like There are things that should probably be a property on the model instead because they're just derived state out of the model. And There are also, you know, many other things, and this list goes on, and come talk to me in the hallway after
if you want to see some of the boundary lines I draw around this. And so, you know, why would you do it this way? I'm gonna go through this uh pretty quickly. We're separating the code that performs the core business logic from the code that's responsible for validating untrusted user User input, verifying user identity, and in many cases say checking permissions. And yeah, checking permissions could be an operations call or it could be a uh a permission from REST framework or built into Django. I think that's where the gray area of this is. And this isn't a one-size-fits-all type thing. It's a general way to structure your application. And um You know, another way reason you do it this way is that a developer that comes into your code
base that wants to know like what's going on here. They don't have to look at the views and they don't have to look at the models, they just have to look at the operations. Because the operations clearly say, sign up, they clearly say accept invitation, follow invitation, create invitation, resend invitation. They see where the core logic lives, what models the core logic interacts with, and then they can also search that operation and find what views are calling it. And you see now we have the core of the applications put into its place where it's mediating between the views and the models and other things like that. And uh yeah, you model your domain and business logic around what you want it to be and make the framework work for you instead of you trying to fit things into the framework's way of doing things.
So again, this is going to be better shown by example, but it's natural to want to create a bunch of methods on a model because Django already gives you the model. It's natural to want to put code in the view because Django already gives you the view and it's already there. That's like the path of least resistance. But if you keep doing that over time and that's the only thing you do, you'll find things get quite oftentimes messy and complicated. And this extra layer brings a lot of simplification. And so I just was summarizing those three things there as notes for later. And I'm gonna do like literally a one-minute demo. I don't think we're gonna have time for questions, so you can ask those in the hallway after. Since we do a lot of agency work, uh we keep building the same thing over and over again. So we're gonna release that as a product
some point in the next month or two. Uh it's called Better Base and it includes it's a full power Django and React starter kit. And out of the box it includes support for invitations. And I don't know how easy that diagram is to see, but you could just imagine this concept of you have multiple accounts that you can belong to, and if you're an owner of an account, you can invite other people to be members of your account. Django uses a membership model on some of the docs as well. So if you're familiar with that, it's similar to that concept. And so I want to show really quick the operations for invitations and then we'll be done. And before I show that, just know that invitations don't fit a CRUD model. They don't really fit a create, read, update, delete, and so they are an interesting case study for this.
You have the inviter who can create or send the invitation, potentially update it. resend it or resend it and the invitee can follow it, accept it or And then accept it or decline it potentially. And then in the background, invitations might also be able to expire, which there's ways you can do that where you don't need to use background tasks. And so let's take a quick look. If we can all see this. And this is going to take just a minute, but one of the ways a developer can get up to speed very quickly with a code base is come into the operations folder here for invitations. So there's one for memberships, invitations, and many other things.
And you can fold all in your code editor. And you can actually get a sense in one minute, which is part of the demo here, of what the heck the invitation system is doing. So we're validating that someone can create an invitation, namely the initiator being some membership to the current account, and an email that we're inviting. Can you create it? Can you update it? Can you resend it? Can you follow it? Okay, and there's a bunch of validate cans. And then it looks like there's something to create the invitation. It looks like there's something to send it as well. And then one of the mistakes I've made, which I decided to intentionally keep in this talk, is that mark invitation as sent is actually only ever called here.
And it is a one of those things that should probably be underscored because it should never be called on its own. You should only ever be calling send invitation. Because that's the fully encapsulated logic. And so I refold stuff. I can come down here, I can see, I can update an invitation, I can get it by a secret token. And then I can follow it. And again, same thing with mark invitation as followed. And look, this is like literally one minute. I can see I can accept it, decline it, delete it, and I could keep going. But the point here is that you can actually see what's happening in this whole complex system that has close to 1500 lines of code, if not more. And a few thousand lines of tests.
In just a couple minutes, by folding the code and looking at the operations that are non-underscored, you now know all the critical pieces of business logic that this application is performing. And you didn't have to look at the views, you didn't even have to look at the models, you just looked at the operations. So that's my presentation. That's what an operation is. And consider experimenting and exploring with as your app grows, how could you implement this or something like this. Thank you.
Models represent and store database state, views provide entry points for retrieving or changing that data, and templates present the resulting information and UI to users. Together, these three layers cover the straightforward version of a Django application.
Discussed at 8:06Use an operations layer when business logic starts interacting with multiple models or other parts of the system, rather than leaving that coordination in one model method or view. It is especially useful when the same business action must be called from different entry points, such as a view, management command, or background task.
Discussed at 12:44An operation is a Python object—such as a function, class, method, decorator, or context manager—that performs application business logic and lives in an `ops` folder or file. It should do something meaningful and be callable by non-operation code; returned data containers do not count as operations.
Discussed at 14:15A helper that should only be called as part of a larger operation should remain an internal dependency, often indicated with a leading underscore. Code also should not become an operation if it genuinely belongs elsewhere, such as derived state that is better represented as a model property.
Discussed at 18:10Operations separate core business logic from request-specific concerns such as validating untrusted input, identifying users, and checking permissions. They also give developers a single, searchable place to understand the application’s main actions and which models those actions coordinate.
Discussed at 19:47Note: We understand that names change, people change, and bodies change. We respect each individual's journey and privacy. If you have any concerns about a video or need us to remove content, please don't hesitate to contact us. We will handle your request with care and promptly address any issues.
Published July 15, 2026
Published July 15, 2026
Published July 15, 2026
Published July 15, 2026
Published July 15, 2026
Published July 14, 2026