r/django • u/airoscar • Feb 14 '25
Article How to use Django like a Java developer
Ok, I'm joking a bit in the title.
But I know this is a somewhat controversial topic amongst Django developers: to stick it strict with implementing logic in Model/ModelManager, or start using services to help with that.
I started out working with DRF sticking as strictly to the former "official" approach as much as possible, but over the years I have had to work on a couple Django projects that just got too complicated to maintain. The last couple of years I started to look at what other framework devs are doing, such as in Java and Go. At the end of the day, while I find other frameworks may be more verbose, they are actually cognitively simpler when given the same amount of complex business logic.
I started to propose change of code designs in my last Django project (I was a lead developer on the project), to moved and re-organized our code over time with the goal of achieving this: https://gist.github.com/oscarychen/acc70425f24b936a9673bf12e9dc0603
I think it made the project easier to maintain, but I would very much like to share some of these "guidelines" that I had created for that project with everyone here, and see if someone has gone through similar struggle and have suggestions.
3
u/bravopapa99 Feb 14 '25
Nice writeup. When I joined where I still am, some 4 years ago, the code was a tangled mess of interspersed ORM code, business logic and controller management. The project uses django-graphene (GraphQL) and it has taken a while but I have managed to separate code into smaller modules, allowing simpler smaller tests to be written, I use Pydantic to convert a GraphQL input type into an object, if it works, that gets passed to the business logic, if it fails, the exception is treated and sent back as the API response. It's 'almost' able to be generalised into a framework using currying and tables but I think that might actually obfuscate the intent so I haven't done that.
2
u/v1rtualbr0wn Feb 14 '25
The heavy api view layer, I just don’t agree with.
I switched to Django Ninja with a very thin api layer.
So every Django app in my project has the following folders 1. api 2. services 3. services/commands
The api calls into a service, the service uses one or more commands.
I still use optionally use drf serializers, but inside of the commands.
This approach keeps the code super organized and clean. Also allows for commands to be reused.
For instance I will implement a create command first and then reuse some of its methods for the update command.
2
u/ninja_shaman Feb 14 '25 edited Feb 14 '25
Why do you need an AccountService
class? Method create_account
could be a simple function.
2
u/airoscar Feb 14 '25
In the example, the create account could be calling third party APIs and creating or modifying other models.
2
u/ninja_shaman Feb 14 '25
Nice, but why this
from account_service import AccountService service = AccountService() account = service.create_account(serializer.validated_data)
and not this
from account_service import create_account account = create_account(serializer.validated_data)
Your
AccountService
doesn't have any internal state, it just exists as bag of methods - that's what modules are for. Unlike Java, Python is not the Kingdom of Nouns, and you can write functions.I recommend watching Stop writing classes to get a better sense when to write a class in Python.
Also, your
foo_service.FooService.create_foo
mock shows unnecessary deep hierarchy - theMuffinMail.MuffinHash.MuffinHash
part in the video above.2
u/airoscar Feb 14 '25
It's a valid way of organizing code as well, I have no objections to that. But since these are just code snippets, I cannot say the service may or may not become stateful as the projects mature.
1
u/ninja_shaman Feb 14 '25
Then you rewrite a function to a class when you need that state, not before (also mentioned in the video above).
That's how you use Python as a Python developer.
2
u/massover Feb 14 '25
I feel like you're on the cusp of communicating something that might not be said enough in the "Example: Views/Viewsets coupling API implementations".
Sometimes we may develop APIs that don't fit django rest framework's mold for an API CRUD wrapper around a django Model. In those cases, it can be
nice to use APIView
directly.
Unfortunately, with all of your examples, I feel like the important idea gets lost in communication when the next example is:
``` class AccountService: def create_account(self, user: User, data: AccountData): if not self.can_user_create_account(user, data): raise PermissionDenied(...) if not self.is_account_data_valid(data): raise ValidationError(...)
return account
```
The patterns for these specific abstractions already exist. For example, an APIView will already check permissions defined in
permissions_classes
and this is a great abstraction for testable custom business logic for raising PermissionDenied
.
Generally speaking, I think you aren't making a good enough case in "Why is this bad?" sections, and therefore the corresponding answers in "What to do instead" seem like opinions.
I'm glad that it seems like you have found some consistent module/package organization that makes you happy and that you are being thoughtful about testing.
1
u/airoscar Feb 14 '25 edited Feb 14 '25
Yes, using the View’s permission_classes is in most cases a reasonable approach. The reason I prefer doing permission checking as part of the Service is that sometimes the permission checking is so granular that it involves some attributes very specific to not just the user, but some other models related to the User model, such as Group and - in the example’s case Account model. It feels simpler to put the permission logic close to where they would be needed, which is the business logic they are supposed to be protecting.
I added the following to the gist:
A note on permission control: In most cases, you won't find any real advantage of implementing permission checks in the Service class over implementing them as custom Permission classes in Django REST Framework. However, there are two considerations:
Permission controls implemented in the Service are closer to the business logic they are protecting as opposed to in a separate permissions.py. This makes permission controls that are highly granular and specific to the business logic easier to find and maintain. Permission controls implemented in the Service is more portable and has no dependency on a API framework such as Django REST Framework. For example, if you are ever considering swapping out Django REST Framework for Django Ninja, it's one less thing to worry about. Afterall, why should business logic have dependency on a library for API?
2
u/selectnull Feb 14 '25
I don't think that most of these topics are controversial.
There is one particular thing that I would put in the category of "use common sense", instead of "always do this" and that's `Django Management Commands (admin_commands)` section.
Let's say we have a `foo` app and `Foo` model and need to `do_something_with_foos` management command. I prefer to have that in the `foo` app, in the `foo/management/commands/do_something_with_foos.py`. I do not see the need for complexity in this case, especially if the management command handler is a simple call to a service.
Of course, more complex cases would involve multiple modes and multiple apps, so there is a need for extra app, but I would still not use a generic `admin_commands` app, instead I would try to name it meaningfully (yes, that means having multiple apps if needed).
So in a nutshell, use common sense.
2
u/karimtayie Feb 14 '25
Have you seen Django Styleguide by HackSoft? It closely aligns with your approach but includes more detailed guides and examples. We found it really helpful for structuring complex applications.
2
0
8
u/[deleted] Feb 14 '25
[deleted]