One day I was looking at a beautiful monolith, she smiled back at me then I went on to make some changes to a file which contained couple of thousand lines of code and a couple of dozen routes. Then I tought, how would I structure this if I had ten minutes to think about it? Let’s find out.
Edit: I added an adapter layer, I think this is a way to separate domains without creating a mess or a too complex structure. I also added a package called unique
for example.
So this is an: Example for separating parts of a flask project for better maintainability and scalability in an aggressively simple way.
This idea is based upon slicing your application with blueprints, but that topic is not detailed here, this is only about the additional separation of building blocks.
Prepare yourself for a world of simplicity and free yourself from the horrors of mental overhead, let’s hit the monolith road like it’s 2010.
Project description
This is a simple flask project, with endpoints that can store and list numbers and letters.
List numbers:
curl -X GET http://127.0.0.1:5000/numbers/
{
"numbers": [
123
]
}
Store number, and get number list:
curl -X POST http://127.0.0.1:5000/numbers/42
{
"numbers": [
123,
42
]
}
This is the same for letters. I would prefer to use a standard like REST or JSON API, but this is a simple example, and this way you can easily try it.
The project has a (you’re) welcome page as well for serving a static html file, for the examples sake.
The whole thing is sort of silly, but it is a good example for a project structure.
Project structure
.
├── app.py
└── src
├── common
│ └── infrastructure
│ └── cache.py
├── letters
│ ├── blueprint.py
│ ├── controller.py
│ ├── repository.py
│ └── service.py
├── numbers
│ ├── blueprint.py
│ ├── controller
│ │ ├── add_controller.py
│ │ └── list_controller.py
│ ├── error_handler.py
│ ├── repository.py
│ └── service
│ ├── numbers_service.py
│ └── validator.py
├── unique
│ ├── adapter.py
│ ├── blueprint.py
│ ├── controller.py
│ └── service.py
└── welcome
├── blueprint.py
├── controller.py
└── templates
└── index.html
app.py
- Main entry point for the application.letters
- Package for handling letters. Contains a blueprint, controller, repository and service modules.numbers
- Package for handling numbers. Contains a blueprint, controller, repository and service modules.welcome
- Package for handling the welcome page. Contains a blueprint and controller and it has a template for the index page.common
- Package for handling project wide needs for example infrastructure related things. Contains a cache module.
Building blocks
This is a basic example for a layered structure, where the building blocks are separated by domains.
Blueprint
- Defines the routes, error handling, and flask related configurations.Controller
- Handles the request and response.Service
- Handles the business logic.Repository
- Handles the database operations.Adapter
- Handles the external domain operations.
Usual relation between building blocks: Blueprint
<- Controller
<- Service
<- Repository
Use only what you need!
If there is no need for a building block, it can be omitted. For example if there is no need to database interaction, there is no need for a repository. An example is the welcome
package, it only has a controller and a blueprint, because it only serves a static page.
This statement is true for flask tools as well, welcome
package has templates, but the others don’t. Everybody has a blueprint, but infrastructure
package doesn’t need one. Speaking of not needing, a package can be only a simple package without the layers, chill.
Split up the fat!
When a module gets too big, it can be split into multiple modules under a package. The numbers
controller is splat into multiple controllers, an add_controller
and a list_controller
, and the error_handler
is moved to a separated module. The service module is splat into multiple services, for example a numbers_service
and a validator
.
Beware of where you connect!
If there is a need for a functionality project wide, it should be in the common
package. For example a cache module, that is used by multiple services.
For another domain’s functionality, use an adapter. For example the unique
package has an adapter, that uses the numbers
and letters
services, for creating a unique list of all the values. The only place where to domains functionality can meet is the adapter.
If there is a need for a type from another domain, I personally would not a do transformation in the adapter layer without meaningful change, but you do you.
The moral
The important thing is to make any separation by domain. Don’t collect all your building blocks per one package (like a project wide controllers), because it will be harder to maintain and scale and review and refactor and staying sober and going to family dinners and… you get the point.
The names of your building blocks doesn’t matter, if you think layered structure should have other layers, do it, if you need read/write separation, do it …as long as it is separated by domains.
But when you decided what will your building blocks be, stick to it, if you choose layered architecture, don’t create another module with an action-domain-responder structure (although you should check it out). So you don’t have to think about the structure, and you’ll have the energy to take care about the business logic. You know, the important part.