{"id":36626218,"url":"https://github.com/everest-engineering/lhotse","last_synced_at":"2026-01-12T09:32:19.100Z","repository":{"id":37275361,"uuid":"255469961","full_name":"everest-engineering/lhotse","owner":"everest-engineering","description":"A starter kit for writing event-sourced web applications following domain-driven design principles. Based on Spring Boot and Axon.","archived":false,"fork":false,"pushed_at":"2024-06-12T05:39:34.000Z","size":3017,"stargazers_count":32,"open_issues_count":3,"forks_count":13,"subscribers_count":7,"default_branch":"main","last_synced_at":"2024-06-12T08:36:14.605Z","etag":null,"topics":["axon","axon-framework","cqrs","domain-driven-design","event-sourcing","spring-boot","starter-kit","starter-project"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/everest-engineering.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}},"created_at":"2020-04-14T00:15:53.000Z","updated_at":"2024-06-12T05:39:37.000Z","dependencies_parsed_at":"2023-12-07T06:22:52.818Z","dependency_job_id":"fce81ccf-e676-480a-b294-489879ce11e4","html_url":"https://github.com/everest-engineering/lhotse","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/everest-engineering/lhotse","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/everest-engineering%2Flhotse","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/everest-engineering%2Flhotse/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/everest-engineering%2Flhotse/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/everest-engineering%2Flhotse/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/everest-engineering","download_url":"https://codeload.github.com/everest-engineering/lhotse/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/everest-engineering%2Flhotse/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28337728,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-12T06:09:07.588Z","status":"ssl_error","status_checked_at":"2026-01-12T06:05:18.301Z","response_time":98,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5: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":["axon","axon-framework","cqrs","domain-driven-design","event-sourcing","spring-boot","starter-kit","starter-project"],"created_at":"2026-01-12T09:32:18.373Z","updated_at":"2026-01-12T09:32:19.094Z","avatar_url":"https://github.com/everest-engineering.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Welcome!\n\n[![Build status](https://badge.buildkite.com/b44f55806fca0e349ecc8d470fe0fdcea8f49c1375f33e86d5.svg?branch=main)](https://buildkite.com/everest-engineering/lhotse) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=everest-engineering_lhotse\u0026metric=alert_status)](https://sonarcloud.io/dashboard?id=everest-engineering_lhotse)\n\nThis is Lhotse, a starter kit for writing event sourced application backends following domain driven design principles.\nIt is based on [Spring Boot](https://spring.io/projects/spring-boot), [Axon](https://axoniq.io/)\nand [Keycloak](https://www.keycloak.org/).\n\nWhether you're starting a new project or refactoring an existing one, you should consider this project if you're\nseeking:\n\n- horizontal scalability with distributed command and event processing\n- crypto-shredding support for your event log to address privacy regulations such as the GDPR\n- deduplicating filestore abstractions for a variety of backing stores such as S3 buckets and Mongo GridFS\n- SSO, role based authorisation and federated identity management\n\n... without the time and effort involved in starting a new project from scratch.\n\nThe only end user functionality provided out of the box is basic support for creating organisations and users. The\nsample code demonstrates end-to-end command handling and event processing flows from API endpoints down to projections.\n\n# Table of Contents\n\n- [Tooling](#tooling)\n    - [IntelliJ configuration](#intellij-configuration)\n    - [Building](#building)\n    - [Semantic versioning](#semantic-versioning)\n    - [Jupyter notebook](#jupyter-notebook)\n    - [Swagger documentation](#swagger-documentation)\n    - [Code style](#code-style)\n- [Features](#features)\n    - [Axon: DDD and event sourcing](#axon-ddd-and-event-sourcing)\n    - [Command validation](#command-validation)\n    - [Event processing](#event-processing)\n    - [Event replays](#event-replays)\n    - [Crypto shredding](#crypto-shredding)\n    - [Security and access control](#security-and-access-control)\n        - [Endpoint access control](#endpoint-access-control)\n    - [Filestore support](#file-support)\n        - [Configuring the In-Memory Filestore](#configuring-the-in-memory-filestore)\n        - [Configuring Mongo GridFS](#configuring-mongo-gridfs)\n        - [Configuring AWS S3](#configuring-aws-s3)\n    - [Media support](#media-support)\n- [Project Info](#project-info)\n    - [Maintainers](#maintainers)\n    - [Contributing](#contributing)\n    - [License](#license)\n\n# Tooling\n\nThis project uses Java 17.\n\nContainer convenience tooling is [Docker](https://www.docker.com/).\n\n[Project Lombok](https://projectlombok.org/) greatly reduces the need for hand cranking tedious boilerplate code.\n\nThe build system is [Gradle](https://gradle.org/).\n\n## IntelliJ configuration\n\nThe [Lombok plugin](https://plugins.jetbrains.com/plugin/6317-lombok/) is required for IntelliJ, else the code generated\nby the Lombok annotations will not be visible (and the project will be littered with red squiggle line errors).\n\n## Building\n\nTo build the entire application, including running unit and functional tests:\n\n`./gradlew build`\n\n(Note that functional tests share the same port number for embedded database as for the containerised database, if tests\nfail try running `docker-compose down` first. Free-up port for the keycloak test server as well).\n\nStart up containers for Postgres and Keycloak:\n\n`docker-compose up`\n\nThis project uses keycloak for authentication and session management. `docker-compose up` will run the keycloak container and\nexpose it on the `KEYCLOAK_SERVER_PORT` specified in the `.env` file.\n\n## Running\nBring up the dependencies:\n`docker compose up`\n\nRun the application server using Gradle:\n`./gradlew bootRun`\n\nTo create a docker image:\n\n`./gradlew bootBuildImage`\n\nTo run the application server container with a TTY attached, allocating 2GiB memory and applying the `prod` Spring\nprofile:\n\n`docker run -t -m 2G --network host -e \"SPRING_PROFILES_ACTIVE=prod\" your.organisation.here/lhotse:$BUILD_VERSION`\n\nTo see all available Gradle tasks for this project:\n\n`./gradlew tasks`\n\nTo run the OWASP dependency check plugin, which will generate a report at `build/reports/dependency-check-report.html`:\n\n`./gradlew dependencyCheckAggregate`\n\nTo run the dependencies license plugin, which will generate a report at `build/reports/dependency-license/index.html`:\n\n`./gradlew generateLicenseReport`\n\n## Semantic versioning\n\n[Semantic versioning](https://semver.org/) is automatically applied using git tags. Simply create a tag of, say, `1.2.0`\nand Gradle will build a JAR package (or a docker container, if you wish) tagged with version `1.2.0-0` where `-0` is the\nnumber of commits since the `1.2.0` tag was applied. Each subsequent commit will increment the version (`1.2.0-1`,\n`1.2.0-2`, ... `1.2.0-N`) until the next release is tagged.\n\n## Jupyter notebook\n\nAn [Jupyter notebook](https://jupyter.org/) can be found in 'doc/notebook'. It acts as an interactive reference\nfor the API endpoints and should be in your development workflow.\n\nJupyter notebook can be run as a Docker container:\n\n`docker-compose -f doc/notebook/docker-compose.yml up`\n\n* Copy the notebook URL with token from notebook container logs.\n* Paste the URL in browser and access the example flows from `work/starter-example`\n\n## Swagger documentation\n\nSwagger API documentation is automatically generated by [springdoc-openapi](https://springdoc.org/v2/).\n\nAPI documentation is accessible when running the application locally by visiting\n[Swagger UI](http://localhost:8080/swagger-ui.html). Default credentials for logging in as an administrator can be\nfound in `application.properties` along with the client ID and client secret.\n\nFunctional tests generate a Swagger JSON API definition at `./launcher/build/web-app-api.json`\n\n## Code style\n\n[Spotless](https://github.com/diffplug/spotless) is run as part of the Gradle build to ensure consistency. Spotless has\nbeen configured to use the Eclipse formatter using the rules in 'build-config/eclipse-formatter-config.xml'. This file\ncan be imported into IntelliJ.\n\nBuild checks will fail if the code is inconsistent with the standard. Automatic formatting can be applied by running:\n\n`./gradlew spotlessApply`\n\nNote that Spotless will not enforce joining of manually wrapped lines in order to keep builder pattern function chaining\nclean.\n\n[PMD](https://pmd.github.io/) and [Checkstyle](https://checkstyle.org/) quality checks are automatically applied to all\nsubprojects.\n\n# Features\n\n## Axon: DDD and event sourcing\n\nPreviously known as Axon Framework, [Axon](https://axoniq.io/) is a framework for implementing\n[domain driven design](https://dddcommunity.org/learning-ddd/what_is_ddd/) using\n[event sourced](https://martinfowler.com/eaaDev/EventSourcing.html)\n[aggregates](https://www.martinfowler.com/bliki/DDD_Aggregate.html) and\n[CQRS](https://www.martinfowler.com/bliki/CQRS.html).\n\nDDD is, at its core, about _linguistics_. Establishing a ubiquitous language helps identify sources of overlap or\ntension in conceptual understanding that _may be_ indicative of a separation of concern in a system. Rather than\nattempting to model a domain in intricate detail inside a common model, DDD places great emphasis on identifying these\nboundaries in order to define [bounded contexts](https://www.martinfowler.com/bliki/BoundedContext.html). These reduce\ncomplexity of the system by avoiding [anemic domain models](https://www.martinfowler.com/bliki/AnemicDomainModel.html)\ndue to a slow migration of complex domain logic from within the domain model to helper classes on the periphery as the\nsystem evolves.\n\nEvent sourcing captures the activities of a business in an event log, an append-only history of every important\n_business action_ that has ever been taken by users or by the system itself. Events are mapped to an arbitrary number of\nprojections for use by the query side of the system. Being able to replay events offers several significant benefits:\n\n* Projections can be optimised for reading by denormalising data\n* Events can be _upcasted_. That is, events are marked with a revision that allows them to be transformed to an updated\n  version of the same event. This protects developers from creating significant errors in users' data due to, for\n  example, accidentally transposing two fields within a command or event;\n* Projections can be updated with new information that was either captured by or derived from events. New business\n  requirements can be met and projections generated such that historical user actions can be incorporated as new\n  features are rolled out;\n\nAxon provides the event sourcing framework. User actions of interest to a business (typically anything that modifies\ndata) are dispatched to command handlers that, after validation, emit events.\n\n## Command validation\n\nCommands represent user actions that may be rejected by the system. Events, however, represent historical events and\ncannot be rejected (though, they can be upcasted or ignored depending on circumstances). It is therefore vital that\nrobust command validation is performed to protect the integrity of the system.\n\nIf events are ever emitted in error then this creates a situation that should only be addressed by generating events\ncountering the erroneous ones. This, naturally, comes with a significant cost in terms of implementation and validation\noverhead.\n\nThere is a philosophical argument for defining aggregates such that all information required to validate commands is\nheld by an aggregate in memory. In practice, however, more natural aggregates can be formed by allowing some validation\nto be based on _projections_. We also know from experience that some validation will be shared among multiple\naggregates. The amount of testing required to verify all possible command failure situations tends to grow non-linearly\nas the number of checks that are performed inside an aggregate grows.\n\nWe have addressed this in the `axon-support` and `command-validation-suport` modules through the introduction of marker\ninterfaces that map commands to dedicated command validators. Validators extract common checks or checks based on\nprojections and allow them to be tested independently. Aggregate tests just need to ensure that a failure in the\nvalidator fails command validation. Since this design opens up the possibility of a validator to be missed, reflection\nis used to detect validators at application start up and register them with a command interceptor that is triggered\nprior to the command handler method being called. This significantly reduces testing effort and human error.\n\n## Event processing\n\nAxon provides two types of event processors,\n[subscribing and tracking](https://docs.axoniq.io/reference-guide/configuring-infrastructure-components/event-processing/event-processors).\n\nSubscribing processors execute on the same thread that is publishing the event. This allows command dispatching to wait\nuntil the event has been both appended to the event store and all event handling is completed. Commands are queued for\nprocessing on a FIFO basis. It is important, therefore, to not use them for long-running tasks.\n\nTracking event processors (TEPs), in contrast, execute in their own thread, monitoring the event store for new events.\nTEPs track their progress consuming events using tracking tokens persisted in the database. TEPs hold ownership of the\ntokens, preventing multiple application instances from concurrently performing the same processing. Token ownership\npasses to another application instance in the event that a token owner is shutdown or restarted.\n\nTEPs introduce additional complexity by not guaranteeing that projections will be up-to-date when an API call has ended.\nTEPs should, in our opinion, be only used for longer running processing, during replays and when preparing projections\nfor new feature releases.\n\nAxon also introduces the concept of processing groups as a way of segmenting and orchestrating event processing,\nensuring that events are handled sequentially within a group. By default, Axon assigns each tracking event processor\n(TEP) to its own processing group, aiming to parallelise event processing as much as possible. We take a more\nconservative approach to make the system easier to reason about by defaulting to subscribing event processors and\nassigning them to a default processing group unless explicitly assigned elsewhere.\n\n## Event replays\n\nEvent replaying takes the system back to a previous point in time in order to apply a different interpretation of what\nit means to process an event.\n\nThe simplest way of executing a replay is to wipe all projections and then reapply every event ever emitted to rebuild\nusing the latest logic. This is a valid approach for fledgling applications but may not be acceptable once the system\nhas scaled up. More advanced approaches are made possible by assigning event processors to different processing groups\nand running a mixture of subscribing and tracking event processors. Advanced configuration opens up the possibility of:\n\n* Replaying events into a new projection database while continuing to project to an existing one, making the replay\n  transparent to end users. The system can then be switched over to use the new projection while optionally continuing\n  to maintain the old one.\n* Tracking event processors can be used to generate projections for new features that are not yet released to users\n  until the projections are ready for use.\n* Processing groups allow replays to be limited to bounded contexts that are naturally isolated.\n\nThe starter kit comes with programmatic support for triggering replays. To perform a replay:\n\n* disconnect the application from load balancers\n* (if running more than a single node) shut down event processing using the `axonserver-cli` or the Axon dashboard\n* trigger a replay via a [Spring Boot Actuator](https://spring.io/guides/gs/actuator-service/) call to `/actuator/replay`\n* monitor the state of replay via a Spring actuator endpoint\n* reconnect the application to load balancers\n\nBehind the scenes, replays are being executed by:\n\n* shutting down [tracking event processors](https://axoniq.io/blog-overview/tracking-event-processors) (TEPs);\n* clearing the tracking tokens in the Axon database;\n* placing a marker event into the event log that will end replays; \n* notifying interested listeners that replays have completed; and\n* restarting TEP processing.\n\n## Crypto shredding\n\nCrypto shredding is a technique for disabling access to sensitive information by discarding encryption keys. You might\nuse this on the behest of a user or when retention is no longer justified in order to comply with the European Union's\nGeneral Data Protection Regulation (GDPR) without compromising the append-only nature of your event log.\n\nDocumentation in the [crypto shredding](https://github.com/everest-engineering/axon-crypto-shredding-extension)\nrepository explains how it works, its limitations and an important caveat.\n\n## Security and access control\n\nWe are using [Keycloak](https://www.keycloak.org/) to manage user authentication and session management. Authorisation\nis handled by the application itself.\n\nKeycloak has the following three main concepts.\n\n* _Realms_ which secure and manages security metadata for a set of users, applications and clients. By default, Keycloak\n  provides a `master`\n  realm which is best used only for superuser administration. We create a separate realm, `default` for managing our\n  application.\n* _Clients_ are the applications on whose behalf Keycloak is authenticating users. By default, Keycloak will provide us\n  with a few clients but using a separate client is the best practice. We have set up a `default client` for\n  the `default` realm.\n* _Roles_ identify a type or category of user. Roles can be specific to a client or apply to an entire realm.\n\n[The official documentation goes into more detail](https://www.keycloak.org/docs/latest/server_admin/index.html#core-concepts-and-terms).\n\n\n### Endpoint access control\n\nController end-points are secured based on user roles, properties and entity permissions. Annotations are used to\nconfigure the access control for each handler method. To reduce repetition and improve readability, a few\nmeta-annotations are created for common security configuration, e.g. `AdminOrAdminOfTargetOrganization`. There are\nsituations when user roles and properties are not sufficient to determine the access control. This is where the entity\npermission check comes in. An entity in this case is a representation of domain object in the application layer. It\ncorresponds to at least one persistable object. For an example, one `Organization` entity corresponds to one\n`PersistableOrganization`. To put it simply in the event sourcing context, it can be just considered as the projection.\n\nThe entity permission check is specified within the security annotation and takes the form\nof `hasPermission(#entityId, 'EntityClassName', 'permissionType')`. This expression is evaluated\nby `EntityPermissionEvaluator`, which in turn delegates to corresponding permission check methods of an entity, where\ncustomized permission requirements can be implemented. This workflow is made possible by: a) having all entity classes\nimplementing the `Identifiable` interface and b) having a `ReadService` for each `Identifable` entity.\nThe `Identifiable` interface provides default _reject all_ permission checks which can be overridden by implementing \nentities. The `ReadService` provides a way to load an entity by its simple class name. To help manage increasing \nnumber of `ReadService`, the starter kit provides a `ReadServiceProvider` bean which collects all `ReadService` beans \nduring start of the application context.\n\nWhen adding new controllers and security configurations, it is important to refer to existing patterns and ensure\nconsistency. This also applies to tests where fixtures are provided to support the necessary _automagic_ behaviours.\n\n## Filestore support\n\nThe [storage](https://github.com/everest-engineering/lhotse-storage) module implements two file stores: one is referred\nto as _permanent_, the other as the _ephemeral_ store. The permanent file store is for storing critical files that, such\nas user uploads, cannot be recovered. The ephemeral store is for non-critical files that can be regenerated by the\nsystem either dynamically or via an event replay.\n\nOur file store implementation automatically deduplicates files. Storing a file whose contents matches a previous file\nwill return a (new) file identifier mapping to the original. The most recently stored file will then be silently\nremoved.\n\nFile stores need backing service such as a blob store or filesystem. This starter kit supports an in-memory filestore,\n[Mongo GridFS](https://docs.mongodb.com/manual/core/gridfs/) and [AWS S3](https://aws.amazon.com/s3/).\n\n### Configuring the In-Memory Filestore\n\nThe in-memory filestore backend is intended only for development and testing. This filestore is not distributed so will\nnot work well when running multiple instances of the application in HA mode. A locally hosted or AWS hosted S3\ncompatible filestore is a better bet in this instance.\n\n```\napplication.filestore.backend=inMemory\n```\n\n### Configuring Mongo GridFS\n\nSet the application property:\n\n```\napplication.filestore.backend=mongoGridFs\n```\n\n### Configuring AWS S3\n\nSet the following application properties:\n\n```\napplication.filestore.backend=awsS3\napplication.filestore.awsS3.buckets.permanent=sample-bucket-permanent\napplication.filestore.awsS3.buckets.ephemeral=sample-bucket-ephemeral\n```\n\nWe rely\non [DefaultAWSCredentialsProviderChain](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html)\nand [DefaultAwsRegionProviderChain](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/regions/DefaultAwsRegionProviderChain.html)\nfor fetching AWS credentials and the AWS region.\n\n## Media support\n\nThe [media](https://github.com/everest-engineering/lhotse-media) module adds additional support for managing of image\nand video updates. It generates thumbnail images on the fly, caching them in the ephemeral file store for subsequent\nrequests. Thumbnail sizes are limited to prevent the system from being overwhelmed by malicious requests.\n\n# Project Info\n\n## Maintainers\n\n[@sluehr](https://github.com/sluehr), [@ywangd](https://github.com/ywangd)\n\n## Contributing\n\nWe appreciate your help!\n\n[Open an issue](https://github.com/everest-engineering/lhotse/issues/new/choose) or submit a pull request for an\nenhancement. You may want to view the [project board](https://github.com/orgs/everest-engineering/projects/1) or browse\nthrough the [current open issues](https://github.com/everest-engineering/lhotse/issues).\n\n## License\n\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n\n[![License: EverestEngineering](https://img.shields.io/badge/Copyright%20%C2%A9-EVERESTENGINEERING-blue)](https://everest.engineering)\n\n\u003e Talk to us `hi@everest.engineering`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feverest-engineering%2Flhotse","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feverest-engineering%2Flhotse","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feverest-engineering%2Flhotse/lists"}