{"id":34066180,"url":"https://github.com/douwevandermeij/fractal","last_synced_at":"2026-01-27T09:32:22.446Z","repository":{"id":50574998,"uuid":"343334152","full_name":"douwevandermeij/fractal","owner":"douwevandermeij","description":"Fractal is a scaffolding toolkit for building SOLID logic for your Python applications.","archived":false,"fork":false,"pushed_at":"2026-01-23T16:20:42.000Z","size":251,"stargazers_count":17,"open_issues_count":0,"forks_count":4,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-01-24T07:19:30.218Z","etag":null,"topics":["cqrs","domain-driven-design","hexagonal-architecture","microservices","ports-and-adapters","python","solid-principles"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/douwevandermeij.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2021-03-01T07:58:27.000Z","updated_at":"2026-01-23T16:20:46.000Z","dependencies_parsed_at":"2024-04-28T08:33:33.566Z","dependency_job_id":"e87da523-219a-49e8-bdff-02ce1e519a37","html_url":"https://github.com/douwevandermeij/fractal","commit_stats":{"total_commits":95,"total_committers":1,"mean_commits":95.0,"dds":0.0,"last_synced_commit":"2a52bb43fae4e28f403ef4ca125a3ee3ddf9a8cf"},"previous_names":[],"tags_count":90,"template":false,"template_full_name":null,"purl":"pkg:github/douwevandermeij/fractal","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/douwevandermeij%2Ffractal","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/douwevandermeij%2Ffractal/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/douwevandermeij%2Ffractal/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/douwevandermeij%2Ffractal/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/douwevandermeij","download_url":"https://codeload.github.com/douwevandermeij/fractal/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/douwevandermeij%2Ffractal/sbom","scorecard":{"id":353837,"data":{"date":"2025-08-11","repo":{"name":"github.com/douwevandermeij/fractal","commit":"d58467b8874bf9ed4fdf6fb4bb82428a7f7fbcef"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.5,"checks":[{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/build.yml:1","Warn: no topLevel permission defined: .github/workflows/publish.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build.yml:22: update your workflow using https://app.stepsecurity.io/secureworkflow/douwevandermeij/fractal/build.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build.yml:25: update your workflow using https://app.stepsecurity.io/secureworkflow/douwevandermeij/fractal/build.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/build.yml:38: update your workflow using https://app.stepsecurity.io/secureworkflow/douwevandermeij/fractal/build.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:12: update your workflow using https://app.stepsecurity.io/secureworkflow/douwevandermeij/fractal/publish.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/douwevandermeij/fractal/publish.yml/master?enable=pin","Info:   0 out of   4 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   1 third-party GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'","Warn: branch protection not enabled for branch 'fix/0.4'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":0,"reason":"59 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: PYSEC-2014-5 / GHSA-296w-6qhq-gf92","Warn: Project is vulnerable to: PYSEC-2011-2 / GHSA-3jqw-crqj-w8qw","Warn: Project is vulnerable to: PYSEC-2012-3 / GHSA-59w8-4wm2-4xw8","Warn: Project is vulnerable to: PYSEC-2012-4 / GHSA-5h2q-4hrp-v9rr","Warn: Project is vulnerable to: PYSEC-2014-6 / GHSA-625g-gx8c-xcmg","Warn: Project is vulnerable to: PYSEC-2015-8 / GHSA-6565-fg86-6jcx","Warn: Project is vulnerable to: PYSEC-2021-98 / GHSA-68w8-qjq3-2gfm","Warn: Project is vulnerable to: PYSEC-2012-2 / GHSA-78vx-ggch-wghm","Warn: Project is vulnerable to: PYSEC-2015-9 / GHSA-7fq8-4pv5-5w5c","Warn: Project is vulnerable to: PYSEC-2015-4 / GHSA-7qfw-j7hp-v45g","Warn: Project is vulnerable to: PYSEC-2011-9 / GHSA-7wph-fc4w-wqp2","Warn: Project is vulnerable to: GHSA-7xr5-9hcq-chf9","Warn: Project is vulnerable to: PYSEC-2014-2 / GHSA-89hj-xfx5-7q66","Warn: Project is vulnerable to: GHSA-8x94-hmjh-97hq","Warn: Project is vulnerable to: PYSEC-2016-2 / GHSA-c8c8-9472-w52h","Warn: Project is vulnerable to: PYSEC-2016-3 / GHSA-crhm-qpjc-cm64","Warn: Project is vulnerable to: PYSEC-2014-4 / GHSA-f7cm-ccfp-3q4r","Warn: Project is vulnerable to: PYSEC-2016-16 / GHSA-fp6p-5xvw-m74f","Warn: Project is vulnerable to: PYSEC-2011-8 / GHSA-fwr5-q9rx-294f","Warn: Project is vulnerable to: PYSEC-2015-5 / GHSA-gv98-g628-m9x5","Warn: Project is vulnerable to: PYSEC-2015-20 / GHSA-h582-2pch-3xv3","Warn: Project is vulnerable to: PYSEC-2011-5 / GHSA-h95j-h2rv-qrg4","Warn: Project is vulnerable to: GHSA-hmr4-m2h5-33qx","Warn: Project is vulnerable to: PYSEC-2015-6 / GHSA-jhjg-w2cp-5j44","Warn: Project is vulnerable to: PYSEC-2016-15 / GHSA-pw27-w7w4-9qc7","Warn: Project is vulnerable to: PYSEC-2015-10 / GHSA-q5qw-4364-5hhm","Warn: Project is vulnerable to: PYSEC-2011-4 / GHSA-rm2j-x595-q9cj","Warn: Project is vulnerable to: GHSA-rrqc-c2jx-6jgv","Warn: Project is vulnerable to: PYSEC-2014-1 / GHSA-rvq6-mrpv-m6rm","Warn: Project is vulnerable to: PYSEC-2014-7 / GHSA-rw75-m7gp-92m3","Warn: Project is vulnerable to: PYSEC-2019-16 / GHSA-vfq6-hq5r-27r6","Warn: Project is vulnerable to: PYSEC-2014-3 / GHSA-wqjj-hx84-v449","Warn: Project is vulnerable to: PYSEC-2011-3 / GHSA-wxg3-mfph-qg9w","Warn: Project is vulnerable to: PYSEC-2011-1 / GHSA-x88j-93vc-wpmp","Warn: Project is vulnerable to: PYSEC-2007-1","Warn: Project is vulnerable to: PYSEC-2008-1","Warn: Project is vulnerable to: PYSEC-2008-2","Warn: Project is vulnerable to: PYSEC-2009-3","Warn: Project is vulnerable to: PYSEC-2015-11","Warn: Project is vulnerable to: PYSEC-2015-7","Warn: Project is vulnerable to: PYSEC-2016-18","Warn: Project is vulnerable to: PYSEC-2018-55 / GHSA-32pc-xphx-q4f6","Warn: Project is vulnerable to: GHSA-hc5x-x2vx-497g","Warn: Project is vulnerable to: GHSA-w3h3-4rj7-4ph4","Warn: Project is vulnerable to: GHSA-29pr-6jr8-q5jm","Warn: Project is vulnerable to: GHSA-g92j-qhmh-64v2","Warn: Project is vulnerable to: GHSA-3ww4-gg4f-jr7f","Warn: Project is vulnerable to: GHSA-9v9h-cgj8-h64p","Warn: Project is vulnerable to: PYSEC-2021-62 / GHSA-hggm-jpg3-v476","Warn: Project is vulnerable to: PYSEC-2017-8 / GHSA-q3cj-2r34-2cwc","Warn: Project is vulnerable to: GHSA-3rq5-2g8h-59hc","Warn: Project is vulnerable to: PYSEC-2021-100 / GHSA-8h2j-cgx8-6xv7","Warn: Project is vulnerable to: PYSEC-2024-38 / GHSA-2jv5-9r88-3w3p","Warn: Project is vulnerable to: PYSEC-2024-232 / GHSA-6c5p-j8vq-pqhj","Warn: Project is vulnerable to: PYSEC-2024-233 / GHSA-cjwg-qfpm-7377","Warn: Project is vulnerable to: PYSEC-2017-28 / GHSA-w799-prg3-cx77","Warn: Project is vulnerable to: GHSA-59g5-xgcq-4qw3","Warn: Project is vulnerable to: PYSEC-2020-150 / GHSA-33c7-2mpw-hg34","Warn: Project is vulnerable to: PYSEC-2020-151 / GHSA-f97h-2pfx-f59f"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-18T08:59:47.189Z","repository_id":50574998,"created_at":"2025-08-18T08:59:47.189Z","updated_at":"2025-08-18T08:59:47.189Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28810550,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-27T07:41:26.337Z","status":"ssl_error","status_checked_at":"2026-01-27T07:41:08.776Z","response_time":168,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cqrs","domain-driven-design","hexagonal-architecture","microservices","ports-and-adapters","python","solid-principles"],"created_at":"2025-12-14T06:22:05.007Z","updated_at":"2026-01-27T09:32:22.426Z","avatar_url":"https://github.com/douwevandermeij.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Fractal\n\n\u003e Fractal is a scaffolding toolkit for building SOLID logic for your Python applications.\n\n[![PyPI Version][pypi-image]][pypi-url]\n[![Build Status][build-image]][build-url]\n[![Code Coverage][coverage-image]][coverage-url]\n[![Code Quality][quality-image]][quality-url]\n\n\u003c!-- Badges --\u003e\n\n[pypi-image]: https://img.shields.io/pypi/v/fractal-toolkit\n[pypi-url]: https://pypi.org/project/fractal-toolkit/\n[build-image]: https://github.com/douwevandermeij/fractal/actions/workflows/build.yml/badge.svg\n[build-url]: https://github.com/douwevandermeij/fractal/actions/workflows/build.yml\n[coverage-image]: https://codecov.io/gh/douwevandermeij/fractal/branch/master/graph/badge.svg\n[coverage-url]: https://codecov.io/gh/douwevandermeij/fractal\n[quality-image]: https://api.codeclimate.com/v1/badges/55adbc041d119d371ef7/maintainability\n[quality-url]: https://codeclimate.com/github/douwevandermeij/fractal\n\n## Installation\n\n```sh\npip install fractal-toolkit\n```\n\n## Usage\n\n* Fractal can be used inside large Python applications to isolate certain (logical related) behaviour from the rest of\nthe application.\n* Fractal is ideal for refactoring large applications into smaller parts\n* Fractal applications by design are microservices\n  * Just wrap the app in an HTTP framework (like FastAPI, see contrib module) and expose with Docker\n  * Other usages apart from HTTP (and Docker) are also possible\n    * Like subscribing to a data stream or pub/sub channel\n\n## Architecture\n\nApplications that use Fractal can be built in many ways, including a non-SOLID architecture.\nThe Fractal toolkit tries to make it easier to go for the SOLID approach.\n\nTo start a Fractal project, the first class to make derives from `fractal.Fractal`.\nIt should provide a `fractal.core.utils.settings.Settings` object and a\n`fractal.core.utils.application_context.ApplicationContext` object, which should also be derived from.\n\nThe `Settings` class provides all static configuration for the application; it's the place where environment variables\nare loaded. The class creates a singleton object.\n\nThe `Context` class provides the dynamic configuration of the application, using the `Settings` object.\nIn the `Context` all dependencies will be injected.\n\n### Hexagonal Architecture (ports and adapters)\n\nIn Hexagonal Architecture, together with Domain Driven Design principles, the core of the application, is the bounded\ncontext containing the domain objects (entities, repositories, services, etc.) but without specific implementation\ndetails. Just the domain logic. From now on we call the core the domain.\n\nThis is (loosely) enforced by not allowing dependencies to external packages inside the domain.\nThis, in turn, is the _dependency inversion principle_ of SOLID.\n\nThe repositories and services inside the domain are interfaces or abstract classes. These are known as ports.\n\nNext to the domain there are the adapters. Each interface or port needs an adapter to function at runtime. Adapters are\nallowed to depend on external packages.\n\nAt runtime, in the application `Context`, based on `Settings`, the appropriate adapter will be set for each port.\n\n### Basic application structure\n\nA typical application folder structure using Fractal looks like:\n\n    app/\n    ├── adapters/\n    │   ├── __init__.py\n    │   └── products.py\n    ├── domain/\n    │   ├── __init__.py\n    │   └── products.py\n    ├── context.py\n    ├── main.py\n    └── settings.py\n\nWith this, a fully functional Fractal application can be built having a Python interface. That is, the logic of the\napplication can only be reached by invoking methods on Python level.\n\nSuch Fractal applications might be used as part of larger (Python) applications to isolate or encapsulate certain\nbehaviour. The larger application itself can also be a Fractal application, and so on. Hence the name: Fractal.\n\nWhile using Fractal as a way to have separation of concerns with separate isolated bounded contexts in Python\napplications, it's also possible to wrap Fractal in a small application and expose as REST API using, for example,\nFastAPI, Flask or Django. Next that application can be deployed again in a Docker environment. This makes Fractal a\nperfect fit for microservices as well.\n\nAs a rule of thumb, continuing on the separation of concerns, the folder/file structure inside a Fractal application\nshould follow the naming of the subject (rather than the naming of the responsibilities of module).\nIn the example app this is denoted by `products.py` in both the `domain` folder as the `adapters` folder.\nWhen the file is getting too big to be easily readable or maintainable, it can be converted into a package.\nWithin the package the files can be named by their responsibilities.\n\nAn example package folder structure:\n\n    app/\n    ├── adapters/\n    │   └── products/\n    │       ├── __init__.py\n    │       ├── django.py\n    │       └── fastapi.py\n    ├── domain/\n    │   └── products/\n    │       ├── __init__.py\n    │       ├── commands/\n    │       │   ├── __init__.py\n    │       │   └── add.py\n    │       └── events.py\n    ├── context.py\n    ├── main.py\n    └── settings.py\n\nAs can be seen in the example package folder structure, in the `domain` the package contains files about certain\nactions or responsibilities andf in the `adapters` folder it's more about the target implementation.\nOf course the target implementation file can be converted into a package again and contain files for certain\nresponsibilities again.\n\n#### Example file contents\n\n##### main.py\n\n```python\nfrom fractal import Fractal\n\nfrom app.context import ApplicationContext\nfrom app.settings import Settings\n\n\nclass ApplicationFractal(Fractal):\n    settings = Settings()\n    context = ApplicationContext()\n```\n\n##### settings.py\n\n```python\nimport os\n\nfrom fractal.core.utils.settings import Settings as BaseSettings\n\n\nclass Settings(BaseSettings):\n    BASE_DIR = os.path.dirname(__file__)\n    ROOT_DIR = os.path.dirname(os.path.dirname(BASE_DIR))\n    APP_NAME = os.getenv(\"APP_NAME\", \"product_system\")\n\n    def load(self):\n        self.PRODUCT_REPOSITORY_BACKEND = os.getenv(\"PRODUCT_REPOSITORY_BACKEND\", \"\")\n```\n\n##### context.py\n\n```python\nfrom fractal.core.utils.application_context import ApplicationContext as BaseContext\n\nfrom app.settings import Settings\n\n\nclass ApplicationContext(BaseContext):\n    def load_repositories(self):\n        from app.domain.products import ProductRepository\n\n        if Settings().PRODUCT_REPOSITORY_BACKEND == \"sql\":\n            '''example: some sql adapter code'''\n        elif Settings().PRODUCT_REPOSITORY_BACKEND == \"file\":\n            '''example: some file adapter code'''\n        else:\n            from app.adapters.products import InMemoryProductRepository\n\n            self.product_repository: ProductRepository = self.install_repository(\n                InMemoryProductRepository(),\n            )\n```\n\n##### domain/products.py\n\n```python\nfrom abc import ABC\nfrom dataclasses import dataclass\n\nfrom fractal.core.models import Model\nfrom fractal.core.repositories import Repository\n\n\n@dataclass\nclass Product(Model):\n    id: str\n    name: str\n\n\nclass ProductRepository(Repository[Product], ABC):\n    pass\n```\n\n##### adapters/products.py\n\n```python\nfrom fractal.core.repositories.inmemory_repository_mixin import InMemoryRepositoryMixin\n\nfrom app.domain.products import Product, ProductRepository\n\n\nclass InMemoryProductRepository(ProductRepository, InMemoryRepositoryMixin[Product]):\n    pass\n```\n\n## Advanced features\n\n### Command bus pattern\n\nA command is a container to invoke actions in the domain, from inside and outside of the domain.\nA command has a one-to-one relation with a command handler.\nThe command handler can be seen as a single transaction, e.g., to a database.\n\nThe code in the command handler should just be doing just the things that are necessary to be inside the transaction.\nTransactions can fail, so it's important to prevent side effects from happening and include only the code that needs to\ngo in the same transaction and thus will be rolled back as a whole in case the transaction fails.\n\nSecondary actions that need to take place _after_ the action has been done, should be outside of scope of the command\nhandler.\n\nAfter a command handler has been completed successfully, that is, when the transaction is persisted, an event can be\npublished. This event is the trigger for all secondary actions, which in turn can be commands again.\n\n#### Example file contents\n\nThe affected files in the folder structure:\n\n    app/\n    └── domain/\n    │   └── products/\n    │       └── commands.py\n    └── context.py\n\n##### commands.py\n\nWithout publishing events:\n\n```python\nfrom dataclasses import dataclass\n\nfrom fractal.core.command_bus.command_handler import CommandHandler\nfrom fractal.core.command_bus.commands import AddEntityCommand\n\nfrom app.context import ApplicationContext\nfrom app.domain.products import Product, ProductRepository\n\n\n@dataclass\nclass AddProductCommand(AddEntityCommand[Product]):\n    pass\n\n\nclass AddProductCommandHandler(CommandHandler):\n    command = AddProductCommand\n\n    def __init__(\n        self,\n        product_repository: ProductRepository,\n    ):\n        self.product_repository = product_repository\n\n    @staticmethod\n    def install(context: ApplicationContext):\n        context.command_bus.add_handler(\n            AddProductCommandHandler(\n                context.product_repository,\n            )\n        )\n\n    def handle(self, command: AddProductCommand):\n        self.product_repository.add(command.entity)\n```\n\n##### context.py\n\n```python\nfrom fractal.core.utils.application_context import ApplicationContext as BaseContext\n\n\nclass ApplicationContext(BaseContext):\n\n    ...\n\n    def load_command_bus(self):\n        super(ApplicationContext, self).load_command_bus()\n\n        from app.domain.products.commands import AddProductCommandHandler\n\n        AddProductCommandHandler.install(self)\n```\n\n### Event publishing\n\nWhen an event gets published, the `EventPublisher` will iterate over its registered projectors (`EventProjector`).\nEach projector will be invoked with the event as a parameter.\n\nProjectors can do anything:\n- printing the event to the console\n- populating a repository\n  - like an event store\n  - or a read optimized view\n- invoking a new command\n- sending the event to an external service, which may:\n  - invoke a new command\n  - send an email\n\nEach projector should only be doing one thing.\nThe relation between an event and a projector is one-to-many.\n\n**!! CAVEAT !!**\n\nWhen using events, and especially when sending events to an external service, be aware that these other services might\nhave a dependency on the structure of the event.\nChanging existing events is **dangerous**.\nThe best approach here is to apply the _open-closed principle_ of SOLID, open for extension, closed for modification.\nAlternatively creating a new event is also possible.\n\n#### Example file contents\n\nThe affected files in the folder structure, on top of the command bus pattern code:\n\n    app/\n    └── domain/\n    │   └── products/\n    │       ├── commands.py\n    │       └── events.py\n    └── context.py\n\n##### commands.py\n\n```python\nfrom dataclasses import dataclass\nfrom datetime import datetime\n\nfrom fractal.core.command_bus.command_handler import CommandHandler\nfrom fractal.core.command_bus.commands import AddEntityCommand\nfrom fractal.core.event_sourcing.event_publisher import EventPublisher\n\nfrom app.context import ApplicationContext\nfrom app.domain.products import Product, ProductRepository\nfrom app.domain.products.events import ProductAddedEvent\n\n\n@dataclass\nclass AddProductCommand(AddEntityCommand[Product]):\n    user_id: str\n\n\nclass AddProductCommandHandler(CommandHandler):\n    command = AddProductCommand\n\n    def __init__(\n        self,\n        event_publisher: EventPublisher,\n        product_repository: ProductRepository,\n    ):\n        self.event_publisher = event_publisher\n        self.product_repository = product_repository\n\n    @staticmethod\n    def install(context: ApplicationContext):\n        context.command_bus.add_handler(\n            AddProductCommandHandler(\n                context.event_publisher,\n                context.product_repository,\n            )\n        )\n\n    def handle(self, command: AddProductCommand):\n        event = ProductAddedEvent(\n            id=command.entity.id,\n            name=command.entity.name,\n            created_by=command.user_id,\n            created_on=datetime.utcnow(),\n        )\n        self.product_repository.add(command.entity)\n        self.event_publisher.publish_event(event)\n```\n\n##### events.py\n\n```python\nfrom dataclasses import dataclass\nfrom datetime import datetime\nfrom typing import Callable, Dict, List, Type\n\nfrom fractal.core.command_bus.command import Command\nfrom fractal.core.event_sourcing.event import (\n    BasicSendingEvent,\n    Event,\n    EventCommandMapper,\n)\n\n\n@dataclass\nclass ProductEvent(BasicSendingEvent):\n    id: str\n\n    @property\n    def object_id(self):\n        return self.id\n\n    @property\n    def aggregate_root_id(self):\n        return self.id\n\n\n@dataclass\nclass ProductAddedEvent(ProductEvent):\n    name: str\n    created_by: str\n    created_on: datetime\n\n\nclass ProductEventCommandMapper(EventCommandMapper):\n    def mappers(self) -\u003e Dict[Type[Event], List[Callable[[Event], Command]]]:\n        return {\n            # example:\n            # ProductAddedEvent: [\n            #     lambda event: SomeCommand(...)\n            # ],\n        }\n```\n\n##### context.py\n\n```python\nfrom fractal.core.utils.application_context import ApplicationContext as BaseContext\n\n\nclass ApplicationContext(BaseContext):\n\n    ...\n\n    def load_event_projectors(self):\n        from fractal.core.event_sourcing.projectors.command_bus_projector import (\n            CommandBusProjector,\n        )\n\n        from app.domain.products.events import ProductEventCommandMapper\n\n        self.command_bus_projector = CommandBusProjector(\n            lambda: self.command_bus,\n            [\n                ProductEventCommandMapper(),\n            ],\n        )\n\n        from fractal.core.event_sourcing.projectors.print_projector import (\n            PrintEventProjector,\n        )\n\n        return [\n            self.command_bus_projector,\n            PrintEventProjector(),\n        ]\n```\n\n### Eventual consistency\n\nTODO\n\n### Event sourcing\n\nTODO\n\n### Specification pattern\n\nTODO\n\n### FastAPI + Docker\n\nTODO\n\nRequest contract, together with URI parameters and authentication token payload can be processed by the application\nby using the command bus. The command can ingest the separate variables and/or domain objects (entities).\n\nResponse contract might be different from the domain object that is affected by the request.\n\n### Authentication\n\nTODO\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdouwevandermeij%2Ffractal","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdouwevandermeij%2Ffractal","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdouwevandermeij%2Ffractal/lists"}