This is a modular monolith Fast API project that uses the latest and greatest tooling (uv, ruff, pyright, pydantic, pytest, fastapi, sqlmodel, etc) attempting to implement a modular monolith architecture. The repository include pre-commit hooks for ruff, pyright, and uv.
For quick setup instructions, jump to the Setup and development section. This project serves as a proof of concept and starting point for building modular monolith applications with FastAPI. For technical details, see Project Implementation
I don't understand the point of modular monolithic, me neither.. a monolith? modules? big ball of mud? actually I never heard of the term "modular monolith" until I started working on this project so i searched around. There is certinly some hype around the idea of modular monoliths, there are framewworks like Lato a Python microframework designed for building modular monoliths and loosely coupled applications. or Actio ... enables you to effortlessly switch between monolithic and microservices architectures. but that doesn't tell me what a modular monolith is!
I collected a few articles trying to explain what a modular monolith, some in support of the idea and some say it doesn't exist whatsoever. Baffling.
What is a modular monolith? | Actually Talking about Modular Monoliths | The Mythical Modular Monolith | Long live the Monolith! Monolithic Architecture != Big Ball of Mud | Modular Monoliths • Simon Brown • GOTO 2018 | Microservices vs Monolithic Architecture | Cross module communication in modular monolith | What Is a Modular Monolith? | Structuring Modular Monoliths | Modular Monolith: A Primer | Modular Monolith: Is This the Trend in Software Architecture? | Microservices Killer: Modular Monolithic Architecture | Modular monolith in Python | Modular Monoliths: Revolutionizing Software Architecture for Efficient Payment Systems in Fintech | How modular can your monolith go? Part 7 - no such thing as a modular monolith?
I think these two fellows in the comments summarize the concensous best:
Don't start with microservices – monoliths are your friend
no one ever talks about architectures in the middle between those two - modular monoliths (KronisLV)
Because there is no newly invented architecture called "modular monolith" - monolith was always supposed to be MODULAR from the start. (ozim)
So... if you read between the lines modular monoliths are just a buzzword for "good software engineering practices"?
If we think about Microservices Communications, the main difference between microservices and modular monoliths is that "One runs on a single process and the other runs on multiple processes". They both apply "Separation of Concerns" and "Single Responsibility" principles, just at different abstraction levels.
The true argument for and against mono-centric vs distributed concern management is Conway's Law
, which states that "organizations design systems that mirror their communication structure". So the choice between monolith and microservices often comes down to team structure - small teams benefit from monoliths, while multiple teams might prefer microservices.
The 2x2 grid above, commonly found in articles about microservices versus modular monoliths, splits software architecture into two axes:
- Logical: How the software is organized and separated
- Physical: Where the software runs
Most GitHub projects claiming to implement modular monoliths actually implement a normal monolith with good separation of concerns. All parts of the software must communicate, typically in one of two ways:
- Function Calls (Local Procedure Calls): Direct function calls between different parts of the software
- RPC (Remote Procedure Calls): Function calls that can work across different machines
If we assume our software is "well-designed" and "modular", we're really only working with the physical axis - determining where the software runs, not how it communicates internally.
The 2x2 grid could be simplified to a single line representing "openness to communication":
A practical example of this is creating a "self-RPC" calling monolith - a REST API application with multiple responsibilities that communicate only through their public API endpoints.
After researching modular monoliths, I decided to implement a practical example that demonstrates these concepts. This project implements a pet adoption system that showcases how to build a well-structured modular application. Here are the requirements that guided the implementation:
- FastAPI and Uvicorn: The application must be built using FastAPI framework and served with Uvicorn
- REST API: Expose a clean, RESTful API interface
- Multiple Services: Support multiple responsibilities through different endpoints
- Internal Communication: Services must communicate through their public REST API endpoints
- Logging: Implement a robust logging system for debugging and monitoring
- Testing: Comprehensive test coverage for routes, services, and custom functionality
- Service Separation: Although monolithic, the application should be designed to be easily split into microservices
- API Documentation: Well-defined and typed API with automatic documentation
- Code Generation: Auto-generated API clients from service specifications using Pydantic models
- Modern Async: Leverage FastAPI and AsyncIO for efficient asynchronous operations
- Isolated Logging: Each service maintains its own logging configuration
The following sections detail how these requirements were implemented, starting with the technical choices and architecture decisions.
The FastAPI docs demonstrates how to generate OpenAPI API bindings for TypeScript and suggests using OpenAPI Generator to generate bindings for other languages. the OpenAPI generator is an nodejs tool and not a python tool so using it breaks our pure python dependency we have had so far.
There are multiple other tools that can generate python bindings for OpenAPI specifications but they are all either abandoned, unmaintained or not working. they may work for you, I couldn't get any of them to work for this project.
Openapi Python Generator |
openapi-python-client |
fastapi-code-generator |
pythogen
OpenAPI Generator offers a variety of customization options, the customization documentation is really long and doesn't mention python at all so I skipped it, here is the command to generate the API:
npx -g openapi-generator-cli generate -g python --library asyncio \
-i generated/openapi.json -o generated/api
see the generate
command for the cli options and the python generator
for python specific options.
you can also use --additional-properties
for more python specific options.
NOTE: the --library asyncio
is a required option in order to be able to self-call the API from within the application. by default the generator uses urllib3
which is blocking and halts the program making it never respond to the request it has generated to itself. (ask me how I know)
how do we generate the OpenAPI specification? FastAPI offers a Swagger UI and ReDoc which themselfs are generated from the OpenAPI specification.
FastAPI exposes an get_openapi
function call that returns the Application OpenAPI specification as a python dictionary. so it's easy to manipulate and save to a file. in this project it is done using a script src/app/subapp.openapi.py called by run.sh before the application starts. the script scans for all the routes exposed by the application including mounts and subapps (which aren't "visible" to the parent application object) to merges them into a single OpenAPI specification.
When diving into the FastAPI documentation (Bigger Applications - Multiple Files) you're introduced to the concept of routers as a way to organize your application routes as inticing as it is to implement into modular monolith architecture we will not be using routers for the services. FastAPI routers are very assessible are intended to to allow easier mangament of the application routes throughout the source code. Instead we will be using Sub Applications - Mounts. the Purpose of sub applications aka Mounts is to create a complete new FastAPI application under the existing one that is invisible to its parent almost completely. it's as if we are executing two separate applications.
Alternatives: we don't have to use mounts, we could just run multiple application instances under the same python process. uvicorn exposes serve
which can be used to orchestrate multiple applications under the same python process, this however forces us to use different ports for each application! this may be desirable and puts us even closer to the microservice architecture.
Bonus point to the alternative: by manutally creating the AsyncIO event loop we can mirror javascript promises and make asyncio act like promises when used with asyncio.create_task
How can I start a Python async coroutine eagerly?
we could go even further and implement Domain-driven design with Python and FastAPI which goes as far as to force "restriction of import between domains" is a little bit of a stretch, so it was only partially implemented
This project has multiple services and all of them are in the same codebase, they could reference and reuse the same routers extending their own functionality with generic functionality or even call common functions. This complexity grows fast and keeping track of which and what executes where can become a nightmare fast. only if there was a way to know which service is executing at any given time!
What is a middleware? from FastAPI docs - Middleware:
A "middleware" is a function that works with every request before it is processed by any specific path operation. And also with every response before returning it.
a middleware is a wrapper around the request and response and it can do whatever it wants with them before they are processed by the path operation.
We can take advantage of this and the fact we use subapps to create a context aware logger! all that's needed is to create a middleware that will set a context variable with the name of the current service. see LoggerContextMiddleware
and getContextualLogger
by applying the middleware to the application with a unique name for each service and getting a contextual logger every time we need to log something we are done! no more confusion about which service is logging what!
This great article explains how to go about logging from asyncio without blocking sepecifically Practice #04. Log From Asyncio Without Blocking it is suggests to use the logging.handlers.QueueHandler
and logging.handlers.QueueListener
to achieve this. which essentially queue away the logging messages and process them in a separate thread. this method is easy to implement using only a configuration file and no new code.
That's all well and good but it doesn't have the word async
in it WHERE IS THE FUN IN THAT? That's a significant inspiration for implementing the AsyncEmitLogHandler which is a logging handler that emits log records as async tasks instead of "running a thread" (it's essentialy the same thing under the GIL
model) both methodologies operate in a concurrent manner.
following mCoding's video and partially using his configuration file the log_config.json is a configuration file for the logging system that uses the AsyncEmitLogHandler
to emit log records as async tasks in both simple plain text and json formats while also keeping the colored uvicorn text output in the console, (see also: uvicorn logger definition)
NOTE: for formatting options and properties of loggers see LogRecord attributes
logging formatters LogRecord attributes
This project implements a pet adoption system with two main services:
- User Service
src/services/user_service
- Pet Service
src/services/pet_service
These services demonstrate a many-to-many relationship where users can adopt multiple pets, and pets can be adopted by multiple users. The services are configured through config.yaml and combined into a single application via src/app/main.py.
-
Pet Service: Manages pet-related operations (create, read, update, delete)
- Pet attributes: name, species, age, mood, feeding time, interaction time
- Standalone service that doesn't know about users
- Provides endpoints for basic pet management and interactions (like feeding)
-
User Service: Handles user operations and pet adoption
- User attributes: name, ID
- Maintains relationships between users and their adopted pets
- Communicates with the Pet Service through its public API
- Does not duplicate pet data, only stores relationships
Both services use SQLModel for database operations, combining SQLAlchemy's power with Pydantic's data validation. Data is stored in a SQLite database that persists between application restarts.
- Adopting a Pet
# User 1 adopts Pet 1
curl -X 'POST' \
'http://127.0.0.1:8000/user/1/pets/1' \
-H 'accept: application/json' \
-d ''
Response shows the user with their newly adopted pet:
{
"name": "John Doe",
"id": 1,
"pets": [
{
"name": "Fluffy",
"species": "cat",
"age": 2,
"mood": "sleepy",
"id": 1,
"last_fed": "2024-12-18T21:03:51.790256",
"last_interaction": "2024-12-18T21:03:51.790311"
}
]
}
- Interacting with a Pet
# Give a treat to Pet 1
curl -X 'POST' \
'http://127.0.0.1:8000/pet/1/treat' \
-H 'accept: application/json' \
-d ''
Response shows the pet's updated state:
{
"name": "Fluffy",
"species": "cat",
"age": 2,
"mood": "excited",
"id": 1,
"last_fed": "2024-12-19T04:16:29.585504",
"last_interaction": "2024-12-19T04:16:29.585531"
}
Each service provides its own Swagger documentation:
- Main API documentation: http://127.0.0.1:8000/docs
- User Service API: http://127.0.0.1:8000/user/docs
- Pet Service API: http://127.0.0.1:8000/pet/docs
For a complete list of available endpoints and their specifications, see the OpenAPI specification.
Before starting, ensure you have the following installed:
- Python 3.13 or higher
- Node.js and npm (for OpenAPI generation)
- UV package manager
Check your installations:
python --version
npm --version # Should be 10.8.2 or higher
uv --version # Should be 0.5.8 or higher
- Clone the repository:
git clone https://github.com/YoraiLevi/modular-monolith-fastapi
cd modular-monolith-fastapi
- Set up the Python environment:
uv sync
source ./.venv/bin/activate
- Start the application:
./run.sh
More ways to run the application:
python -m app # run the aggregate application
uvicorn services.user_service:app --reload # run the user service with default service configuration
uvicorn services.pet_service:app --reload # run the pet service with default service configuration
Once running, you can access:
- Main API documentation: http://127.0.0.1:8000/docs
- User Service API: http://127.0.0.1:8000/user/docs
- Pet Service API: http://127.0.0.1:8000/pet/docs
Logs are written to both the console and the logs
directory.
This repository includes a preconfigured VS Code debugging setup in launch.json. To use it:
- Open the project in VS Code
- Navigate to the Debug panel (Ctrl+Shift+D)
- Select "FastAPI" from the debug configuration dropdown
- Start debugging (F5)
The project uses pytest for testing. To run tests:
# Run all tests
pytest
# Run tests with coverage report
pytest --cov=src
# Run specific test file
pytest tests/test_specific_file.py
# Run tests in verbose mode
pytest -v
- If you encounter database errors, try deleting the SQLite database file and restarting the application
- For OpenAPI generation issues, ensure npm is properly installed and the openapi-generator-cli is accessible
-
Kraken Technologies: How we organise our very large Python monolith - reduce imports with importlinter
-
Microservices Design Principles for Well-Crafted Architectures
-
Monolithic vs Microservice Architecture: Which To Use and When?
-
Its Time to go Back to the Monoliths. Use Modular Monolith, save costs
-
Struggling to get my head around monoliths vs microservices with FastAPI
-
Microservice in Python using FastAPI - full guide including docker image
-
How can I add unknown arguments in a POST request in FastAPI using BaseModel?
-
How to get a list of mounted sub applications from a FastAPI app?
-
Debug FastAPI in VS Code IDE & Debug FastAPI application in VSCode
-
coralogix's Python Logging Best Practices: The Ultimate Guide
-
Python Logging: How to Write Logs Like a Pro! - Big mistake with logging is that it stores data and sometimes that data is passwords in plain text.
-
How to configure FastAPI logging so that it works both with Uvicorn locally and in production?
-
geeksforgeeks' Dependency Injection in FastAPI example & FastAPI Advanced Dependencies
-
Pytest with Eric - comprehensive pytest book
-
Generate unique operationIds only when there is a duplicate - OpenAPI operationId generation conflict for manual subapps merging
-
Async IO It Is, but Which One? - few big-name alternatives that do what asyncio does, albeit with different APIs and different approaches, are curio and trio. Python Advanced: Mypy vs Pyright — A Detailed Comparison with Examples - Advanced Configuration Examples Create a modern pre-commit setup for Python using UV, Ruff and more -
pre-commit install
executing all files,otherwise stagedpre-commit run --all-files -v
https://github.com/fastapi/full-stack-fastapi-template
https://github.com/zhanymkanov/fastapi-best-practices
https://github.com/arctikant/fastapi-modular-monolith-starter-kit
https://github.com/r2rstep/modular-monolith
https://github.com/BrianThomasMcGrath/modular-monolith
https://github.com/aipress24/aipress24
https://github.com/rifatrakib/fast-subs
https://github.com/vasilmkd/fastapi-eureka-docker-service-discovery
https://github.com/sabatinim/fast_api_hello_world
https://github.com/jod35/fastapi-beyond-CRUD
- FastAPI in Containers - Docker
- The Architecture Behind a One-Person Tech Startup (2021)
- Build Real-World AWS Microservices with Python and FastAPI From Zero
- WSGI vs ASGI for Python Web Development
- What is the difference between Uvicorn and Gunicorn+Uvicorn?
- jsonschema
- pytest-asyncio
- pytest-mock
- python-json-logger
- gh-act - run github actions locally
gh act --secret-file .env
- Maybe I misunderstood how uvicorns --reload-exclude is supposed to work but it doesn't seem to work for me, this caused the
watchfile
logger to be created and log messages to be printed to the console. and self-update itself when used with--reload
andDEBUG
settings. it blocked the entire application. - there are multiple
main.py
each one is intended to be an entrypoint for a service or an aggregate application. as it is now they aren't all operational. - figure out how to allow
--reload
to work with app.main launch script
- vscode will not commit if there are any unstaged changes in the working directory when pre-commit hooks are exist - pre-commit
- Keep getting "307 Temporary Redirect" - the
"status: OK"
path originally used to be"/"
rather than""
but this causes redirects that break the test - Can I override fields from a Pydantic parent model to make them optional? - service model files issue an error for overriding fields in the base model, src/services/user_service/models.py src/services/pet_service/models.py - But why do it that way? because FastAPI example code is doing it that way(
name: str | None = None
forHeroUpdate
butname: str
forHeroBase
). - session.exec does not support delete statements