You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This is a modular monolith Fast API project that uses the latest and greatest tooling ([uv](https://github.com/astral-sh/uv), [ruff](https://github.com/astral-sh/ruff), [pyright](https://github.com/microsoft/pyright), [pydantic](https://github.com/pydantic/pydantic), [pytest](https://github.com/pytest-dev/pytest), [fastapi](https://github.com/fastapi/fastapi), [sqlmodel](https://github.com/fastapi/sqlmodel), etc) attempting to implement a modular monolith architecture. The repository include pre-commit hooks for ruff, pyright, and uv.
4
4
5
-
For project setup and usage jump to the [Setup and development](#setup-and-development) section.
6
-
This project isn't intended to be a production ready application, it's a proof of concept and a starting point for a modular monolith Fast API application.
7
-
for implementation details see [Choosing technology](#choosing-technology)
5
+
For quick setup instructions, jump to the [Setup and development](#setup-and-development) section.
6
+
This project serves as a proof of concept and starting point for building modular monolith applications with FastAPI.
7
+
For technical details, see [Choosing technology](#choosing-technology).
8
8
9
9
## Architecture and Design
10
10
@@ -47,54 +47,58 @@ So... if you read between the lines modular monoliths are just a buzzword for "g
47
47
48
48
#### What does the modular monolith solve?
49
49
50
-
if we think about [Microservices Communications](https://medium.com/design-microservices-architecture-with-patterns/microservices-communications-f319f8d76b71) really hard we can see that the main difference between microservices and modular monoliths
51
-
"One runs on a single process and the other runs on multiple processes" summarzing everything, maybe too quickly, they're both applying "Seperation of Concerns" and "Single Responsibility" principles on a different abstraction level... so what's the difference?
52
-
true argument for and against mono-centric vs distributed concern management is [`Conway’s Law`](https://www.youtube.com/watch?v=TqhkWaeUN_8) which states that "organizations design systems that mirror their communication structure".
53
-
So the difference is our team. if we are a small team we should build a monolith and if we have multiple teams we should build a microservice. knowingly develop software with Conway's law in mind.
50
+
If we think about [Microservices Communications](https://medium.com/design-microservices-architecture-with-patterns/microservices-communications-f319f8d76b71), 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.
54
51
55
-
### Communication between software components
52
+
The true argument for and against mono-centric vs distributed concern management is [`Conway's Law`](https://www.youtube.com/watch?v=TqhkWaeUN_8), 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.
This 2x2 grid is found in many articles and videos about microservices vesus modular monoliths it splits the space of software into two axes, logical and physical.
60
-
logical - How the software is bunched or separated together
61
-
physical - Where the software is running
62
-
Most github project that I found which claim to implement a form of modular monolith are actually just implement a form of a normal monolith with good seperation of concerns. at some point all the parts of the software have to communicate with each other. traditionally there are two ways to do this:
58
+
The 2x2 grid above, commonly found in articles about microservices versus modular monoliths, splits software architecture into two axes:
59
+
-**Logical**: How the software is organized and separated
60
+
-**Physical**: Where the software runs
61
+
62
+
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:
63
63
64
-
1. Function calls - "Local" Procedure call - Some part of the software exposes a function that can be called by another part of the software.
65
-
2. RPC - Remote Procedure call - Some part of the software exposes a function that can be called by another part of the software maybe on a different machine even.
64
+
1.**Function Calls** (Local Procedure Calls): Direct function calls between different parts of the software
65
+
2.**RPC** (Remote Procedure Calls): Function calls that can work across different machines
66
66
67
-
So if we are taking our software to be "welldesigned" and "modular" to begin with we are left with only one axis and it's the physical axis which only tells us where the software is running and not how it's communicating with the other parts of itself.
67
+
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.
68
68
69
-
The 2x2 grid should actually be a line from left to right over the "openness to communication" axis.
69
+
The 2x2 grid could be simplified to a single line representing "openness to communication":
A simple example of way to implement a "self-RPC" calling Monolith is to create a "monolithcal" REST API software with multiple responsibilities that only communicate with each other through their public API endpoints.
73
+
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.
74
+
75
+
### Project Implementation
76
+
77
+
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:
74
78
75
-
###Choosing technology
79
+
#### Core Requirements
76
80
77
-
Let's see our basic requirements for the project:
81
+
***FastAPI and Uvicorn**: The application must be built using FastAPI framework and served with Uvicorn
82
+
***REST API**: Expose a clean, RESTful API interface
83
+
***Multiple Services**: Support multiple responsibilities through different endpoints
84
+
***Internal Communication**: Services must communicate through their public REST API endpoints
85
+
***Logging**: Implement a robust logging system for debugging and monitoring
86
+
***Testing**: Comprehensive test coverage for routes, services, and custom functionality
78
87
79
-
* It must use python FastAPI and Uvicorn
80
-
* It will expose a REST-like API
81
-
* It will have multiple responsibilities with multiple different endpoints
82
-
* Internally the software must communicate with its own parts through the public REST API
83
-
* The software should incooporate a robust logging system
84
-
* The software should have tests for all the routes, services and custom functionality
88
+
#### Additional Features
85
89
86
-
Let's add some more requirements, for fun:
90
+
***Service Separation**: Although monolithic, the application should be designed to be easily split into microservices
91
+
***API Documentation**: Well-defined and typed API with automatic documentation
92
+
***Code Generation**: Auto-generated API clients from service specifications using Pydantic models
93
+
***Modern Async**: Leverage FastAPI and AsyncIO for efficient asynchronous operations
94
+
***Isolated Logging**: Each service maintains its own logging configuration
87
95
88
-
* The software is monolithic but should be able to be split into multiple services instantly
89
-
* The public API should be well defined and typed
90
-
* The public API should be autogenrated from the service specifications and use Pydantic models
91
-
* The software will implement as much as possible using the FastAPI framework and AsyncIO tools
92
-
* Each service should have its own logging configuration without mixups
96
+
The following sections detail how these requirements were implemented, starting with the technical choices and architecture decisions.
93
97
94
98
#### Generating OpenAPI API bindings for Python's AsyncIO
95
99
96
-
The FastAPI demonstrates how to [generate OpenAPI API bindings for TypeScript](https://fastapi.tiangolo.com/advanced/generate-clients/#generate-a-typescript-client) and suggests using [OpenAPI Generator](https://openapi-generator.tech/) 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.
97
-
There are multiple other tools that can generate python bindings for OpenAPI specifications but they are all either abandonedor not maintained not working or confusing to use. they may work for you, I couldn't get any of them to work for this project.
100
+
The FastAPI docs demonstrates how to [generate OpenAPI API bindings for TypeScript](https://fastapi.tiangolo.com/advanced/generate-clients/#generate-a-typescript-client) and suggests using [OpenAPI Generator](https://openapi-generator.tech/) 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.
101
+
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.
see the [`generate`](https://openapi-generator.tech/docs/usage#generate) command for the cli options and the [`python generator`](https://openapi-generator.tech/docs/generators/python#config-options) for python specific options.
112
117
you can also use `--additional-properties` for more python specific options.
113
118
**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)
@@ -151,24 +156,45 @@ following [mCoding's video](https://youtu.be/9L77QExPmI0?si=qy7VcJ0aciWt2D7X&t=1
151
156
**NOTE:** for formatting options and properties of loggers see [LogRecord attributes](https://docs.python.org/3/library/logging.html#logrecord-attributes)
This project implements two CRUD services ["user"](src/services/user_service) and ["pet"](src/services/pet_service) with a many-to-many relationship between them. they're configured through [config.yaml](config.yaml) and aggregated into a single application via [src/app/main.py](src/app/main.py)
157
-
A user can adopt multiple pets and a pet can be adopted by multiple users. the pet service is unaware of user service. the user service keeps track of the pets adopted by the user and requests data about pets from the pet service, it does not mirror the pet service's data.
158
-
these two services are implemented through the [sqlmodel](https://sqlmodel.tiangolo.com/) library which is a "marriage" between sqlalchemy and pydantic with an SQLite database that is saved persistently to disk.
161
+
This project implements a pet adoption system with two main services:
159
162
160
-
The models, "entities" for the two services are defined in [src/services/user_service/models.py](src/services/user_service/models.py) and [src/services/pet_service/models.py](src/services/pet_service/models.py) respectively.
161
-
The routes and business logic are defined in [src/services/user_service/main.py](src/services/user_service/routes.py) and [src/services/pet_service/main.py](src/services/pet_service/main.py) respectively.
163
+
1. User Service [`src/services/user_service`](src/services/user_service)
164
+
2. Pet Service [`src/services/pet_service`](src/services/pet_service)
162
165
163
-
User experience is important so a user can adpopt a pet and when recieving a response data about themselves they can see all the pets they have adopted.
164
-
```
165
-
# User 1 Adpts Pet 1
166
+
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](config.yaml) and combined into a single application via [src/app/main.py](src/app/main.py).
* Pet attributes: name, species, age, mood, feeding time, interaction time
172
+
* Standalone service that doesn't know about users
173
+
* Provides endpoints for basic pet management and interactions (like feeding)
174
+
175
+
***User Service**: Handles user operations and pet adoption
176
+
* User attributes: name, ID
177
+
* Maintains relationships between users and their adopted pets
178
+
* Communicates with the Pet Service through its public API
179
+
* Does not duplicate pet data, only stores relationships
180
+
181
+
Both services use [SQLModel](https://sqlmodel.tiangolo.com/) for database operations, combining SQLAlchemy's power with Pydantic's data validation. Data is stored in a SQLite database that persists between application restarts.
182
+
183
+
#### Example Operations
184
+
185
+
1.**Adopting a Pet**
186
+
187
+
```bash
188
+
# User 1 adopts Pet 1
166
189
curl -X 'POST' \
167
190
'http://127.0.0.1:8000/user/users/1/pets/1' \
168
191
-H 'accept: application/json' \
169
192
-d ''
170
193
```
171
-
```
194
+
195
+
Response shows the user with their newly adopted pet:
196
+
197
+
```json
172
198
{
173
199
"name": "John Doe",
174
200
"id": 1,
@@ -184,18 +210,21 @@ curl -X 'POST' \
184
210
}
185
211
]
186
212
}
187
-
188
213
```
189
214
190
-
further more they can give them a treat and see the pets react to it!
215
+
2.**Interacting with a Pet**
191
216
192
-
```
217
+
```bash
218
+
# Give a treat to Pet 1
193
219
curl -X 'POST' \
194
220
'http://127.0.0.1:8000/pet/pets/1/treat' \
195
221
-H 'accept: application/json' \
196
222
-d ''
197
223
```
198
-
```
224
+
225
+
Response shows the pet's updated state:
226
+
227
+
```json
199
228
{
200
229
"name": "Fluffy",
201
230
"species": "cat",
@@ -206,43 +235,94 @@ curl -X 'POST' \
206
235
"last_interaction": "2024-12-19T04:16:29.585531"
207
236
}
208
237
```
209
-
for the full route list see the [OpenAPI specification at generated/openapi.json](generated/openapi.json)
238
+
239
+
#### Available API Endpoints
240
+
241
+
Each service provides its own Swagger documentation:
242
+
* Main API documentation: [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs)
243
+
* User Service API: [http://127.0.0.1:8000/user/docs](http://127.0.0.1:8000/user/docs)
244
+
* Pet Service API: [http://127.0.0.1:8000/pet/docs](http://127.0.0.1:8000/pet/docs)
245
+
246
+
For a complete list of available endpoints and their specifications, see the [OpenAPI specification](generated/openapi.json).
210
247
211
248
## Setup and development
212
249
213
-
make sure you have uv and npm installed on the machine.
214
-
```
215
-
npm --version
216
-
# 10.8.2
217
-
```
218
-
```
219
-
uv --version
220
-
# uv 0.5.8
221
-
```
250
+
### Prerequisites
251
+
252
+
Before starting, ensure you have the following installed:
logs are written to the console and to the logs directory.
283
+
### Accessing the Application
284
+
285
+
Once running, you can access:
286
+
* Main API documentation: [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs)
287
+
* User Service API: [http://127.0.0.1:8000/user/docs](http://127.0.0.1:8000/user/docs)
288
+
* Pet Service API: [http://127.0.0.1:8000/pet/docs](http://127.0.0.1:8000/pet/docs)
289
+
290
+
Logs are written to both the console and the `logs` directory.
236
291
237
292
### Debugging and Testing
238
293
239
-
this repository includes a [launch.json](.vscode/launch.json) file for vscode debugging armed with a FastAPI launch settings.
240
-
Execute tests with `pytest`
294
+
#### VS Code Debugging
241
295
242
-
```
296
+
This repository includes a preconfigured VS Code debugging setup in [launch.json](.vscode/launch.json). To use it:
297
+
298
+
1. Open the project in VS Code
299
+
2. Navigate to the Debug panel (Ctrl+Shift+D)
300
+
3. Select "FastAPI" from the debug configuration dropdown
301
+
4. Start debugging (F5)
302
+
303
+
#### Running Tests
304
+
305
+
The project uses pytest for testing. To run tests:
306
+
307
+
```bash
308
+
# Run all tests
243
309
pytest
310
+
311
+
# Run tests with coverage report
312
+
pytest --cov=src
313
+
314
+
# Run specific test file
315
+
pytest tests/test_specific_file.py
316
+
317
+
# Run tests in verbose mode
318
+
pytest -v
244
319
```
245
320
321
+
#### Common Issues
322
+
323
+
* If you encounter database errors, try deleting the SQLite database file and restarting the application
324
+
* For OpenAPI generation issues, ensure npm is properly installed and the openapi-generator-cli is accessible
325
+
246
326
## More references
247
327
248
328
*[Kraken Technologies: How we organise our very large Python monolith](https://blog.europython.eu/kraken-technologies-how-we-organize-our-very-large-pythonmonolith/) - reduce imports with importlinter
0 commit comments