I do exactly this in my side project. I have a set of rules which put restrictions on which packages and modules can be included from other packages and modules. For example, a high maturity package is not allowed to depend upon a low maturity package. Similarly, a core library package is not allowed to rely on a package that is specific to a particular product or a particular piece of bespoke development. In this way, much of the potential for circular dependencies is eliminated, and the purpose and internet is clearly communicated.
(I don't do this using sourcery though ... I have my own set of rules)
Basically, you have an api class for each Django app, and you use this class for all external interactions. The api class calls the service class, and the service class deals with the Django ORM. I added a view class, which is my DjangoRestFramework layer; so when a request comes in, it's caught by my view class, and passed onto the api class. I have DRF serializers for outgoing data, and pydantic schemas for incoming data. I also have a selector class for read-only views of my data.
It's a lot of typing, but I know exactly where everything is when something goes wrong, or I need to add a small adjustment somewhere, also it's easy for new devs to learn and use. One downside is that an api change require you to touch a dozen files.
Not really, no. But it is pretty straightforward. My projects have:
- apis.py - the "external" surface of the app
- views.py - DRF layer on top of apis.py
- services.py - the layer that writes to the Django ORM
- selectors.py - provides "read-only" views or filters of data
- serializers.py - serialized outputs, using DRF
- schemas.py - pydantic classes that control incoming JSON types
- models.py - Django model declaration
- urls.py - url endpoints, pointing to views.py
- core.py - maybe another file with more business logic, used by apis.py
I have had trouble using Django API Domains interfaces.py, so I left them out. The main point is, figure out the right balance of concise code and separation of concerns for your taste, and your stack. Good luck!
Thank you, that's very helpful.
Do I understand correctly that the REST component uses 2 different data structures? schemas.py for the incoming JSON and serializers.py for the return data?
Yes, according to the way I did it. You could put DRF serializers and Pydantic schema into the same file and call that "serializers.py", or you could just use DRF for incoming form validation.
Similarly, you could collapse "selectors.py" into "services.py". I put read-only operations into "selectors.py" and write operations into "services.py", but you don't have to. I got that idea from this styleguide: https://github.com/HackSoftware/Django-Styleguide which is in the Appendix of the Django Domain API docs.
That's very cool.
Can you tell a bit more about this set of rules?
"For example, a high maturity package is not allowed to depend upon a low maturity package. Similarly, a core library package is not allowed to rely on a package that is specific to a particular product or a particular piece of bespoke development."
I really like these.
I have a general system for representing metadata in source files. (I use YAML documents embedded in block comments).
Some of this metadata gives traceability information for requirements, tests etc.. while other metadata enables me to associate a maturity level with each file.
My build system understands this metadata and uses it to inform e.g. the minimum test coverage that it expects on a file-by-file basis.
The same metadata is used to ensure that all of the other components that a file references are at the same level of maturity or higher.
I also have metadata for each file (partly derived from location in the repository) that gives each file a number which defines it's position in a hierarchy of design elements.
The position in the hierarchy helps to indicate what the purpose of the file is. I use this to make a distinction between those core, foundational, stable design elements upon which other design elements may build, and those more peripheral, ephemeral and 'agile' design elements which can be quickly tailored to meet the needs of a client or partner.
This means that a (hopefully stable) core API component can be prevented from relying upon a (perhaps less stable) bespoke customer-specific component. It also means that there's more freedom in changing and adapting peripheral designs as you can have confidence that it's stability is not something that is going to be relied upon.
Thanks for the detailed description.
That's a really sophisticated system with several cool features.
* minimum test coverage on a file-by-file basis
* various levels maturity
"It also means that there's more freedom in changing and adapting peripheral designs as you can have confidence that it's stability is not something that is going to be relied upon."
That's a big advantage, indeed.
I also like the concept of storing this metadata next to the code in structured comments.
(I don't do this using sourcery though ... I have my own set of rules)