Skip to content

Commit 2b8463f

Browse files
authored
Merge pull request #106 from febus982/refactor
Application structure refactor
2 parents fc54306 + d105e12 commit 2b8463f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+459
-319
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ COPY --chown=nonroot:nonroot poetry.lock .
6868
COPY --chown=nonroot:nonroot src/alembic ./alembic
6969
COPY --chown=nonroot:nonroot src/domains ./domains
7070
COPY --chown=nonroot:nonroot src/gateways ./gateways
71-
COPY --chown=nonroot:nonroot src/common ./common
71+
COPY --chown=nonroot:nonroot src/bootstrap ./bootstrap
7272
COPY --chown=nonroot:nonroot src/alembic.ini .
7373
COPY --chown=nonroot:nonroot Makefile .
7474

docker-compose.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ services:
1717
environment:
1818
OTEL_SERVICE_NAME: "bootstrap-fastapi-worker"
1919
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector:4317"
20+
CELERY__broker_url: "redis://redis:6379/0"
21+
CELERY__result_backend: "redis://redis:6379/1"
2022
volumes:
2123
- './src:/app'
2224
- './pyproject.toml:/app/pyproject.toml'
@@ -33,6 +35,8 @@ services:
3335
environment:
3436
OTEL_SERVICE_NAME: "bootstrap-fastapi-worker"
3537
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector:4317"
38+
CELERY__broker_url: "redis://redis:6379/0"
39+
CELERY__result_backend: "redis://redis:6379/1"
3640
volumes:
3741
- './src:/app'
3842
- './pyproject.toml:/app/pyproject.toml'
@@ -56,6 +60,8 @@ services:
5660
environment:
5761
OTEL_SERVICE_NAME: "bootstrap-fastapi-dev"
5862
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector:4317"
63+
CELERY__broker_url: "redis://redis:6379/0"
64+
CELERY__result_backend: "redis://redis:6379/1"
5965
ports:
6066
- '8000:8000'
6167
volumes:
@@ -86,6 +92,8 @@ services:
8692
environment:
8793
OTEL_SERVICE_NAME: "bootstrap-fastapi-http"
8894
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector:4317"
95+
CELERY__broker_url: "redis://redis:6379/0"
96+
CELERY__result_backend: "redis://redis:6379/1"
8997
ports:
9098
- '8001:8000'
9199
volumes:

docs/api-documentation.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ The example `books` domain provides 2 endpoints to demonstrate this approach
1414
* `/api/books/v2` (POST)
1515

1616
/// note | Media type versioning
17+
1718
An improvement could be moving to [media type versioning](https://opensource.zalando.com/restful-api-guidelines/#114)
1819
///

docs/inversion-of-control.md

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,31 +38,36 @@ the concrete class whenever the protocol is expected:
3838
def main():
3939
Container()
4040
service = BookService()
41-
42-
# file `books/_data_access_interfaces.py`
41+
42+
43+
# file `books/_gateway_interfaces.py`
4344
class BookRepositoryInterface(Protocol):
4445
async def save(self, book: BookModel) -> BookModel:
4546
...
4647

47-
# file `domains/books/service.py`
48-
from domains.books._data_access_interfaces import BookRepositoryInterface
48+
49+
# file `domains/books/_service.py`
50+
from domains.books._gateway_interfaces import BookRepositoryInterface
4951
from dependency_injector.wiring import Provide, inject
5052

53+
5154
class BookService:
5255
book_repository: BookRepositoryInterface
5356

5457
@inject
5558
def __init__(
56-
self,
57-
book_repository: BookRepositoryInterface = Provide["book_repository"],
59+
self,
60+
book_repository: BookRepositoryInterface = Provide["book_repository"],
5861
) -> None:
5962
self.book_repository = book_repository
6063

61-
# file `common/di_container.py`
64+
65+
# file `bootstrap/di_container.py`
6266
from sqlalchemy_bind_manager._repository import SQLAlchemyAsyncRepository
6367
from dependency_injector.containers import DeclarativeContainer
6468
from dependency_injector.providers import Factory
6569

70+
6671
class Container(DeclarativeContainer):
6772
# Note that dependency-injector package only allows string references
6873
book_repository: Factory[BookRepositoryInterface] = Factory(
@@ -122,24 +127,27 @@ concrete classes because nested imported modules, solving nothing.
122127
///
123128

124129
```python
125-
# file `domains/books/service.py`
130+
# file `domains/books/_service.py`
126131
from dependency_injector.containers import DynamicContainer
127-
from domains.books._data_access_interfaces import BookRepositoryInterface
132+
from domains.books._gateway_interfaces import BookRepositoryInterface
133+
128134

129135
class BookService:
130136
book_repository: BookRepositoryInterface
131137

132138
def __init__(
133-
self,
134-
container: DynamicContainer,
139+
self,
140+
container: DynamicContainer,
135141
) -> None:
136142
self.book_repository = container.book_repository()
137143

144+
138145
# entrypoint
139-
from domains.books.service import BookService
146+
from domains.books._service import BookService
140147
from dependency_injector.providers import Factory
141148
from sqlalchemy_bind_manager._repository import SQLAlchemyAsyncRepository
142149

150+
143151
def main():
144152
container = DynamicContainer()
145153
# Note that dependency-injector package only allows string references
@@ -196,18 +204,20 @@ the life cycle of the concrete classes is handled in the correct order
196204
and we don't end up in circular dependencies.
197205
///
198206

199-
200207
```python
201-
# file `common/factories.py`
202-
from domains.books._data_access_interfaces import BookRepositoryInterface
208+
# file `bootstrap/factories.py`
209+
from domains.books._gateway_interfaces import BookRepositoryInterface
210+
203211

204212
def book_repository_factory() -> BookRepositoryInterface:
205213
from sqlalchemy_bind_manager._repository import SQLAlchemyAsyncRepository
206214
return SQLAlchemyAsyncRepository()
207215

208-
# file `domains/books/service.py`
209-
from domains.books._data_access_interfaces import BookRepositoryInterface
210-
from common.factories import book_repository_factory
216+
217+
# file `domains/books/_service.py`
218+
from domains.books._gateway_interfaces import BookRepositoryInterface
219+
from bootstrap.factories import book_repository_factory
220+
211221

212222
class BookService:
213223
book_repository: BookRepositoryInterface
@@ -258,13 +268,13 @@ We would need to implement the functionalities a dependency injection container
258268
///
259269

260270
```python
261-
# file `common/injectors.py` (Theoretical)
271+
# file `bootstrap/injectors.py` (Theoretical)
262272
def inject_book_repository(f):
263273
@functools.wraps(f)
264274
def wrapper(*args, **kwds):
265275
# This allows overriding the decorator
266276
if "book_repository" not in kwds.keys():
267-
from gateways.storage import BookRepository
277+
from bootstrap.storage import BookRepository
268278
kwds["book_repository"] = BookRepository()
269279
elif not isinstance(kwds["book_repository"], BookRepositoryInterface):
270280
import warnings

docs/packages/bootstrap.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Bootstrap
2+
3+
The `bootstrap` package contains logic that is shared among the external layer
4+
(i.e. `http_app`, `celery_worker`, etc.).
5+
6+
It contains the following submodules and packages (and related responsibilities):
7+
8+
* `bootstrap.bootstrap`: The application initialisation logic (database, logging,
9+
celery tasks) necessary to run the domain logic. It uses `bootstrap.config` and
10+
`bootstrap.di_container` subpackages. It does not contain the specific HTTP
11+
framework initialisation (or other frameworks such as GRPC).
12+
* `bootstrap.config`: The application config models, based on `BaseSettings`
13+
and `BaseModel` from `pydantic` package to get the values from
14+
environment variables.
15+
* `bootstrap.di_container`: The dependency injection container configuration.
16+
* `bootstrap.storage`: The storage configuration (SQLAlchemy). This setup uses
17+
[Imperative Mapping](https://docs.sqlalchemy.org/en/20/orm/mapping_styles.html#imperative-mapping)
18+
so that our models remains simple classes.
19+
20+
/// warning | Note about SQLAlchemy ORM Imperative Mapping
21+
22+
Even if the code for models appears to remain simple classes, imperative mapping
23+
transforms them behind the scenes. However, the code in our application should not
24+
rely on such specific capabilities otherwise we would bind our code to SQLAlchemy.
25+
26+
To handle database operations we use a repository class that is aware of SQLAlchemy.
27+
In this way, should we need to change our storage implementation (e.g. switch to MongoDB),
28+
we'll only need to change the repository class, without having to change anything in
29+
our application logic.
30+
///

docs/packages/celery_worker.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Celery worker
22

3-
The `celery_worker` module is a small entrypoint to run Celery workers and beat.
3+
The `celery_worker` package is a small entrypoint to run Celery workers and beat.
44

55
The `Celery` class has to be initialised to invoke tasks from domain logic,
66
in addition to the worker, therefore we initialise it together with the generic

docs/packages/common.md

Lines changed: 0 additions & 15 deletions
This file was deleted.

docs/packages/domains.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Domains
22

3-
The `domains` module contains all the application domain logic separated by domains
3+
The `domains` package contains all the application domain logic separated by domains
44
(this template provides a single domain: `books`).
55

66
Each domain should be self-contained and not invoke logic from other domains directly.
@@ -15,4 +15,34 @@ Using interfaces will:
1515

1616
## Book domain structure
1717

18-
[TODO] Refactor needed to implement a better structure to the domain
18+
The `domains.book` package provides an example implementation for a domain. It contains
19+
a list of public and protected modules.
20+
21+
Public package and modules are used by the application to invoke the
22+
domain functionalities:
23+
24+
* The main `domains.book` package provides the entrypoint for our application:
25+
the `BookService` class. We export it here from the `domains.book._service`
26+
module to hide protected entities that should not be accessed directly.
27+
* The `domains.book.interfaces` provides the `BookServiceInterface` protocol
28+
to be used for Inversion of Control (we don't currently use it in this
29+
application because Clean Architecture doesn't enforce Inversion of Control
30+
from the `http` application, and we don't have yet implemented other domains)
31+
* The `domains.book.dto` provides the data transfer objects required to invoke
32+
the `BookService` class.
33+
* The `domains.book.events` provides the event data structures that the domain
34+
is able to emit. They can be used by other domains to implement event handlers.
35+
36+
Protected package and modules are used by the implementation of the books domain
37+
and can be used to bootstrap the application:
38+
39+
* The `domains.book._gateway_interfaces` contains the gateway protocols against
40+
which the domain logic is implemented. We use them to configure the dependency
41+
injection container.
42+
* The `domains.book._models` contains the domain models. We use them also
43+
to bootstrap the SQLAlchemy imperative mapping in `bootstrap.storage.SQLAlchemy`
44+
package.
45+
* The `domains.book._service` contains the `BookService` implementation.
46+
* The `domains.book._tasks` contains the implementation of celery tasks
47+
for operations that can be queued without waiting for a result (e.g.
48+
send an email, invalidate cache).

docs/packages/gateways.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Gateways
22

3-
The `gateways` module contains the implementations of the drivers
3+
The `gateways` package contains the implementations of the drivers
44
handling communication with external services (i.e. database repositories,
55
event producers, HTTP clients).
66

docs/packages/http_app.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# HTTP App
22

3-
The `http_app` module contains the implementation of [FastAPI](https://fastapi.tiangolo.com/)
3+
The `http_app` package contains the implementation of [FastAPI](https://fastapi.tiangolo.com/)
44
framework and the logic relevant to HTTP communication (routes, graphql schema, HTTP error handling)

0 commit comments

Comments
 (0)