Business logic: what's the effort to switch between approaches?

Question

Do you think it’d be easier to switch a project from using service/selector functions for business logic to using custom querysets/model managers for business logic or vice versa? Or about the same effort in either direction?

Context

I’ve been consuming a bunch of info (including many existing forum posts on here) about views and where to put “business logic.” A high-level summary of my freshly minted mental model for this topic:

  • Most people want views to default to being about the request/response exclusively without business logic in them, but it’s fine to make a justified exception to this default if there’s a good reason.
  • The two recommended ways to include business logic (since it’s not in the views) are:
    • use functions located in files named services / selectors or similar (a la Hacksoft styleguide, RAPID architecture, etc.)
    • use custom model managers / queryset managers (a la James Bennett’s two posts (1 & 2), Django Views The Right Way, etc.)
    • In between those above two extremes, there’re a bunch of opinions that say something like, “we basically subscribe to [one of the above recommendations], but what works for us is to have the following modifications/well-defined exceptions to it: [blah blah blah]”
  • Another option (usually seen recommended to beginners) is to keep the business logic in the views until a view is painful (usually defined as long and/or confusing) and then refactor some or all of the business logic out of the view in one of the ways described above.

Also it seems like huge chunks of the discussions are focused more around organizational tradeoffs (aka: can you find what you need when you need it?) and human behavior (aka: making it hard to do problematic coding practices and easy to write “better” code) rather than inherent technical problems with either approach (e.g. in Django structure for scale and longevity, a creator of the Hacksoft styleguide Radoslav Georgiev says, “You can achieve absolutely the same thing with services and selectors with custom managers and custom query sets but this kind of makes you want to use them in APIs and tags and templates and so on. So it’s basically the same, but we just define a nice boundary around it.").

I’ve seen several knowledgeable voices say something along the lines of “you can be successful with whichever way you lean as long as you’re paying attention and not just on auto-pilot when making decisions about what’s right for your project/team”.

I’ve also seen several comments that say, “we tried it [this way], but switched to [that way].”

My overall conclusions/interpretations are:

  • it’s a subjective decision that is highly dependent on the requirements of your project and your personal (or team) preferences
    • there is no “most benefit” specific practice - as in, even though both extremes imply that “most” projects would benefit from their way, it really hasn’t been proven out in terms of adoption that one way would benefit “most” projects. There is a “most benefit” general practice - as in, most projects would benefit from adopting something for standards to address this topic, but you have to individually look at the options and decide what that “something” is for your specific project.
  • you can’t know which approach is “right” for your project until you try one in practice
  • it seems like applying either of the approaches would be roughly the same level of effort to implement
  • the consequences of picking one approach that you later deem to be “wrong” for your project comes down to how much code you’d have to change/move to the replacement approach and ensuring that nothing gets messed up while you’re changing/moving to the replacement approach. So the earlier you know the better, but you still can’t know which is “right” for your project until after you’ve already tried one anyway. In other words, there aren’t secondary consequences of changing approaches outside of moving/reformatting your code in line with the new approach.

So in other words, as long as the code is consistent in how it’s organized/formatted, it kind of doesn’t really matter which approach you choose until something specific comes up in your project or for your team and provides a justification that it would be better to use a different approach.

Agree? Disagree? Thoughts more than welcome.

Specific-to-me context

I don’t have a team that is dictating their preferences on this choice to me. I’m writing my Django project with the known intent to eventually have others be doing the code development. Because I don’t know who I’d be handing development off to, I can’t know their preferences for something like this right now.

I also don’t have enough experience to know what I personally prefer either, and at this point having read all these things, leaning toward either extreme seems valid (:person_shrugging:). I think I need to “just pick something” using my best guess and move forward to see what happens.

“Just picking a way that seems right” seems okay to do for my immediate predicament of wanting to have more organized/consistent code that I don’t cringe at when trying to edit a view, but I also don’t want to make a decision now that will make life unnecessarily hard for whomever I hand it off to in the future (and/or my future self).

It seems to me that if the choices are roughly equivalent and either one can be successful, then I probably want to lean toward first trying whichever approach is easier to switch away from in the future (if there is one). That way, there’s a little less pain if I or others later decide that this early decision was not the best choice for the project.

2 Likes

Bottom line first: You’ve done excellent work here with identifying the key issues within this topic. Your “overall conclusions” section seems spot-on to me.

<opinion>

My guess would be this - but I’ve ever encountered a situation where I’ve needed (or wanted) to do a wholesale conversion. But then I’m also of the opinion that this isn’t a strict “either/or” situation. There’s no technical requirement that all business logic be physically located together. My larger systems tend to have a more hybrid approach.

I wouldn’t even put it this strongly. I think you could put a period after the word “choose”, and end it there. My wording would be more like this:

“As long as the code is consistent in how it’s organized/formatted, it kind of doesn’t really matter which approaches you choose.”

(note the use of the plural here)

“Better” is always a subjective matter of opinion, and a lot of it is affected by what languages and frameworks that you have worked with in the past. I am not aware of any technical issue inherently affecting these decisions.

This, but also …

(and model methods)

… when a particular operation is working with only one model (for managers) or instance thereof (for model methods).

However, having an absolute hard & fast rule (including this one :grin:) tends to create problems either way, because it fails to account for the range of possibilities that exist - along with having to make the judgement call of what you consider “business logic” in some trivial cases.
(There are also code-organizational differences that can appropriately apply with CBVs that don’t apply to FBVs.)

My opinion has always been that if I can see the entire logic of the view on my screen, that’s a lot better than having to bounce across multiple files to find the logic of a couple of functions that are only used by that view. (Even doing something twice isn’t necessarily enough for me to refactor it out. It’s when I’m performing an operation for the third time that I will definitely look for opportunities to refactor that piece of logic. And such restructuring tends to be a trivial operation.)

I also believe that it’s far more important how you implement that business logic rather than where that code is located. A poor algorithm in a function is far more damaging to your system than having that function in the “wrong” file.

I cringe whenever I see a project with less than 10,000 total lines of code that has preemptively created multiple layers of indirection, “service layers”, one model per file, trivial setters and getters, or have gone to extremes in applying various general architectural principles such as “DRY”, etc. I consider the effort spent on such things to be misplaced.

People taking a contrary position to this are usually coming from the perspective aquired from other languages (frequently, Java). But Python isn’t Java, and Django isn’t Spring. Many technical constraints within Java simply don’t exist here, and you are needlessly limiting yourself if you write your Python code adhering to those constraints.
Django is most powerful when you take advantage of all the features of the language available to you.
</opinion>

3 Likes

Yeah, I think this is probably a great heuristic…

The key point that I focus on is that there should only be one key pathway where particular business rules are enforced. “When X happens, Y must also happen”. The big issue is not so much where you put your code, but whether you’re having to update multiple locations whenever X or Y change.

You can start in your view, but maybe you add a requirement that when saving a model something else has to happen. So, you move that extra logic into your Form’s save() method. That both simplifies your view, keeping it cleaner and ensures that (as long as you always use the form) there’s no way you can do X without also doing Y.

You then realise that you want to separate the data validation aspect from the model creation step. You drop down to a regular Form, and implement a Manager method that takes the cleaned data, and processes it. (Maybe you add a save() method proxying to this new method so client code is unchanged.)

Eventually — wow, didn’t this project grow! :tada: — you find yourself wanting to separate the mapping to the database role from your application logic. You implement helper classes that sit in front of the ORM (a service layer if you will). Your view code now interacts with these. They’re responsible for making sure X and Y still happen together. (Maybe you maintain the old method on the Manager, proxying to the new way, in case any code needs it.)

(It’s all a bit of a fairy tale without specifics, but you get the idea)

At any given point, moving the logic is no big deal per se. Normally any issue is code hygiene that you’ll need to address anyway. It’s not where you put it, but that there’s only one key pathway that keeps you sane.

A long long time ago, this was my favourite article on this stuff:

It’s still great.

4 Likes

Thank you, @KenWhitesell and @carltongibson; I feel more comfortable making decisions and trying out some new-to-me stuff after reading your responses.

I think this concept:

“Better” is always a subjective matter of opinion, and a lot of it is affected by what languages and frameworks that you have worked with in the past

People…are usually coming from the perspective aquired from other languages

is really relevant. I’m pretty sure I still don’t really understand what a “service layer” actually is, and I’m beginning to suspect that part of my confusion is that its definition seems to morph based on who’s explaining it. A specific example of how this causes me confusion is in RAPID architecture when the author says:

The thing we [James Bennett and the author] agree on here is that a “service layer” is not the answer. It doesn’t help anyone to hide Django under yet another layer of complexity…

and then next describes his recommendation to have “readers” and “actions” which I thought was a “service layer” (and honestly looks exactly like another layer of complexity to me?), especially because he explicitly says they correlate somewhat to Hacksoft’s “services” and “selectors.” Looking closer, he directly states:

I have also deliberately and explicitly rejected the term “services” because I feel it’s too loaded to be useful. Different languages and frameworks already have their own meanings for “services”, “service layers” and “service-oriented architectures”, and so there’s a big risk of confusion.

No shade meant to any authors of course; I may be confused and uncertain (…so no different than usual anyways :joy:), but I think I’m probably still trending in the right directing having read all these things than had I not.

Anyway, a clarifying question re @KenWhitesell ‘s quote:

(There are also code-organizational differences that can appropriately apply with CBVs that don’t apply to FBVs.)

Might you elaborate a little on what this means?

@carltongibson special thanks for going to the trouble to link the article; it resonates / is especially helpful seeing them walk through the code like that.

I think code hygiene is more where I’m going wrong…I’m trying my best but that dang learning comes in its own time sometimes (:joy:) and then with every new learning, it means refactoring all the not so great stuff I did before :sob: such is life :person_shrugging:

I suppose my thinking was that if I could subscribe to an approach (not as hard rules, but as “good enough for now” defaults while I continue practicing/learning enough to actually make better judgment calls on my own) it would help improve my code hygiene. But it seems like y’all are saying focus on the code hygiene and the approaches will follow based on what you need when you need it. But also it’s maybe a bit of a chicken-egg situation…like, the existence of the approaches are supposed to facilitate good code hygiene and if you already have good code hygiene, which approaches you use where doesn’t matter. Anyway, I hear the overall point of “don’t overcomplicate things” loud and clear.

Absolutely - but please allow me to I address a couple of your other points first.

Stick with James’ explanation at Against service layers in Django. It’s written from a “Django perspective” as a “from the inside” view, written by someone who knows Django far better than I ever will.

IMO, it basically boils down to the idea that all business logic resides in a physically separate location from both the models and the views. The concept comes from the architectural pattern where the retrieval of data and the transfer of data from one location to another all need to be separated from the logic that processes that data. The intent is that the data storage and the data retrieval/distribution processes should be interchangeable components.

(There are cases where this pattern is extremely useful - however, I’ve never known it to apply to a Django project.)

As an amateur military historian, and as former USAF member, I tend to mentally divide the concept of “refactoring” into three different categories.

  • Tactical: Changing code within a single method. Adding select_related to a query or using a list comprehension instead of a for statement would be two examples in this category.
  • Operational: This is where I might find identical functionality in multiple locations that could be collected into a single function. Or, I might decide that a function may be better as a model method. Or, I might create some functionality as a class mixin, and change some classes to use that mixin.
  • Strategic: These would be large-scale changes, that would be considered fundamental changes to what the system does.

I’m highlighting this because tactical refactoring should be considered a routine operation. They’re the little things you can fix or improve quickly. It’s not a big deal to do that. The operational changes are still common, but shouldn’t necessarily be considered routine. It’s only the strategic refactoring that you want to generally avoid. (You may still need to do it, but you don’t ever want to consider it common or routine.)

Which brings me around to the question that I deferred.

When I use CBVs, I tend to take advantage of the structure of those CBVs for adding functionality into the functions like form_valid and get_context_data within that CBV. If it’s functionality I will find useful in multiple locations, I might even abstract that out into a mixin or parent class.

However if I’m using FBVs, I might be creating that functionality in functions that would be physically separated from the view.

It is very rare where I would change from an FBV to a CBV - I usually know when I’m starting whether or not I consider a CBV appropriate. It’s less rare, but still unusual for me to convert a CBV to an FBV - but it happens when I misjudge the true needs for that view. But it’s very common for me to pull logic out of an FBV and create a separate function for it - what I would call a tactical change.

1 Like

If I had to say, methods on Models and Managers is generally considered correct for a reason. (It does break down at some scale, but that’s usually some way out, which buys you the time you’re looking for.)

Mostly though it’s about enjoying the process. No code ever written is problem free.

1 Like

Okay, I think I can dig in with a better informed sense now :nerd_face: Thanks so much again everyone.

re CBV/FBV, that’s making sense. This clarification is getting off topic, but if you’ll indulge it briefly, when you say:

It is very rare where I would change from an FBV to a CBV - I usually know when I’m starting whether or not I consider a CBV appropriate. It’s less rare, but still unusual for me to convert a CBV to an FBV - but it happens when I misjudge the true needs for that view.

are there a handful of common heuristics you’re using to determine the “true need” for FBV or CBV? Like, the main heuristic I’ve heard seems to be “if it’s really straightforward CRUD, use a CBV. If it’s a complicated view, use a FBV.” Obviously straightforward/complicated are subjective.

Relatedly, I recently learned (with the help from several forum discussions) that a view isn’t one to one to a webpage, so it’s normal to have one webpage have several related views. Which is to say, if you can split views up like that into more specific chunks (like three views for two forms on the same page or something), what makes a view become more “complicated” in the first place?

Also, my impression is that if someone talks about using a CBV, they’re generally using Django’s generic CBVs and it’s not typical that people choose to make a CBV that’s not one of the generic ones (but is that generally true?).

and

My heuristic is whether or not what I need to do in the view is a “fit” for what my CBVs are designed to handle. It’s not so much a question of “complexity” as it is as issue of “appropriate use”

The Django-provided CBVs are designed to provide straight-forward BREAD (Browse, Read, Edit, Add, Delete - this includes the ListView) of a single model. Once you get beyond that, such as including related models, then you start to effectively override the internal flow of that CBV - and that’s when you start to lose the benefits of those CBVs.

As you’ve mentioned, it is possible to subclass those CBVs to provide a structure for other Use Cases such as “Base Model and Profile” or “Base Model and Formset” (See django-extra-views — Django Extra Views 0.16.0 documentation as an example). So just because you’re going beyond the original Use Case of the Django-provided CBVs, that doesn’t mean that you should abandon them altogether.

I guess the best way for me to comment on your second part of that is that it’s an option that people either aren’t aware of, or don’t think about when building their views. It might also be a tough sell if you only have one or two views in your system that might need something like this.

So, if I forsee the view fitting in a pattern for which I have an appropriate CBV, then I’ll use it. Otherwise, I don’t try to fit the square peg into the round hole and will use an FBV.

Ummm… I’m going to response with a very cautious “Yes?”. (I’d love to see one or two of the topics to understand the context of this statement.)

Unless you’re talking about cases of JavaScript doing an AJAX load of parts of the page, or something similar to that, I’d raise some questions about the accuracy of that.

As a most fundamental definition, “A view is a function that accepts a Request and returns a Response.” If it doesn’t do this, then it’s not a view. (Simply returning HTML in the sense of a page fragment does not make it a view - at least not in Django-terms.)

It’s not so much a question of “complexity” as it is as issue of “appropriate use”

that makes so much sense, as does knowing/thinking about the subclassing you described; I hadn’t really absorbed that as a possibility before but it makes sense now that you mention it :thinking:

Ummm… I’m going to response with a very cautious “Yes?”. (I’d love to see one or two of the topics to understand the context of this statement.).

:sweat_smile::joy:

There were definitely other related helpful posts that I’m struggling to find again to link here, but the one I saved was Multiple forms/actions in single view. The comments by you and philgyford on that post best describe my confusion – as you recommended to that poster, I too need to separate the concept of a “page” from a “view.”

I’d previously been exposed to the fact that a view takes a request and returns a response a lot while learning so I surface-level “knew” that fact, but I didn’t “understand” it (and I still don’t think I fully grasp it intuitively in practice; I definitely have more to learn).

Unless you’re talking about cases of JavaScript doing an AJAX load of parts of the page, or something similar to that, I’d raise some questions about the accuracy of that.

I’ve watched and read some stuff about using htmx, but I’m very new in this area and haven’t really done anything with JS or AJAX at all

1 Like

Thanks for the reference, I do kinda remember that discussion.

The issue there, if I remember correctly, is that the OP wanted buttons to do different things, depending upon certain conditions. So my comments there were oriented to the idea that you could have multiple buttons on a page - where each button posts to a different view. This is what I might describe as the inverse of what I was understanding you to say. (The page is generated by a single view, but can post back to one of a number of views.)

One way I’ve described this in the past is to suggest that you stop describing what you’re seeing in the browser as a “webpage”. Call it a “document”. (Unfortunately, I have found that people tend to use the term “webpage” to actually describe multiple “things” - none of which really express what’s happening except under some relatively rare situations.)

Consider what you think of when using that term.

You can create a document using Microsoft Word. You can create a more complex document using multiple files, all assembled by Word. But when you’re done pulling it together, what you’ve ended up with is a stack of paper.

Now, you could create this document using Word. Or, you could create a different document that looks the same using LibreOffice.

If I give you two identical stacks of paper, are they the same document, even though they were produced by two different files using two different programs?

The same idea applies to what you’re looking at in the browser. If I have different views that all produce the same “Hello World” text, are these all the same “webpage”?

Or, if I have multiple URLs that all cause the same view to be executed, do you consider each of those URLs to be the same “webpage”?

Now, go back far enough - before JavaScript, before Forms and the POST verb, and before CGI scripting on websites - back when a web server delivered the contents of a file to a browser - at that time, there effectively was a one-to-one correlation between that file and what you saw in your browser. The term “webpage” had a well-defined meaning. (A file containing HTML text, served to a browser via a web server.)

But it’s been a long time since then.

And that is why I tend to cringe when anyone talks about a “webpage”.

Going back to this, because it’s something I’ve beat my head against in the past:

Yeah, I think there are very broadly two camps when people say “service layer” within a Django app:

  1. You do your best to insulate your project’s logic from your framework. Sometimes this will be described as Clean Architecture (by the people who like it) or Enterprise Patterns (by the people who don’t, and feel that it’s Java folks trying to force their mindset on top of Django).
  2. You use a separate module or modules for your business logic, instead of putting methods on models or managers, but you have no intention of trying to decouple from Django. As @carltongibson said, “there should only be one key pathway where particular business rules are enforced,” and models and managers don’t feel like the right place to you for whatever reason.

I’m not a fan of (1), but I do tend to use (2). For instance, one app I work on has publishing functionality, where it manages the draft and published versions of various entities (videos, problems, exams, etc.) that can have dependencies between each other. Creating a new version of a publishable entity causes the following things to happen:

  • A new instance of the PublishableEntityVersion model is created.
  • If any dependencies were specified, PublishableEntityVersionDependency instances are created to represent those.
  • The Draft model entry that points to the latest draft version is updated to point to the newly created version.
  • A DraftChangeLogRecord is created to describe exactly what changed (or was reverted, or deleted).
  • If we are in a bulk transaction, our DraftChangeLogRecord is appended to the active DraftChangeLog. Otherwise, a new DraftChangeLog is created.
  • We calculate any side-effects that a change in this entity has to other entities that it is a dependency of, marking those down in the DraftSideEffect model, and updating our dependency hashes.

Right now, that logic is invoked by calling the create_publishable_entity_version() function. It could have been a method in a Manager, but it’s not clear to me where that manager should have gone. Maybe on PublishbleEntityVersion, since that is what gets passed back. But creating that model is maybe 5% of the actual code and work of this function, since all the complexity comes from the bookkeeping and dependency management logic.

FWIW, we also use custom querysets and managers, but they’re usually for more convenient querying of things you get back from the API layer. So if you get a QuerySet[Draft], you can call .with_unpublished_changes() on it. Likewise, there are a few helper methods on the models themselves, but all of these are read-related. Absolutely all mutations must happen through explicit calls to the API functions in the “service” layer. (Though that’s just a convention, and there’s nothing at the code level that prevents that.)

But to echo others here, the important thing is just that you centralize that logic somewhere. If you have a manager method that you call from forty different places in your code and then you refactor to put that in a service module, I don’t think it’s that big a deal in either direction. If you have forty places in your code that each instantiate your model directly and do similar-but-not-quite-identical logic on them, then things get a lot more painful.

Thanks for the discussion! Could someone clarify the main trade‑offs you’re weighing between the two approaches (performance, maintainability, testability)? Also, is there a preferred migration path or deprecation plan if we switch?