{"id":26538591,"url":"https://github.com/thorlauridsen/spring-cloud-java-event-driven","last_synced_at":"2026-04-08T23:32:19.033Z","repository":{"id":283741903,"uuid":"950859272","full_name":"thorlauridsen/spring-cloud-java-event-driven","owner":"thorlauridsen","description":"Spring Cloud Java Event-driven architecture using AWS SQS SNS","archived":false,"fork":false,"pushed_at":"2025-03-21T21:41:14.000Z","size":132,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-21T22:28:05.885Z","etag":null,"topics":["aws","event-driven","event-driven-architecture","gradle","h2-database","java","microservices","sns","spring","spring-boot","springboot","sqs"],"latest_commit_sha":null,"homepage":"","language":"Java","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/thorlauridsen.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":"2025-03-18T19:43:04.000Z","updated_at":"2025-03-21T21:41:17.000Z","dependencies_parsed_at":"2025-03-21T22:38:15.268Z","dependency_job_id":null,"html_url":"https://github.com/thorlauridsen/spring-cloud-java-event-driven","commit_stats":null,"previous_names":["thorlauridsen/spring-cloud-java-event-driven"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thorlauridsen%2Fspring-cloud-java-event-driven","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thorlauridsen%2Fspring-cloud-java-event-driven/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thorlauridsen%2Fspring-cloud-java-event-driven/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thorlauridsen%2Fspring-cloud-java-event-driven/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thorlauridsen","download_url":"https://codeload.github.com/thorlauridsen/spring-cloud-java-event-driven/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244880659,"owners_count":20525515,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["aws","event-driven","event-driven-architecture","gradle","h2-database","java","microservices","sns","spring","spring-boot","springboot","sqs"],"created_at":"2025-03-21T23:18:51.063Z","updated_at":"2026-04-08T23:32:19.027Z","avatar_url":"https://github.com/thorlauridsen.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Spring Cloud Java Event-driven architecture\n\nThis is a sample project for how you can set up a\n[multi-project Gradle build](https://docs.gradle.org/current/userguide/multi_project_builds.html)\nusing [Spring Cloud AWS](https://github.com/awspring/spring-cloud-aws),\n[Java](https://www.java.com)\nand [Event-driven architecture](https://en.wikipedia.org/wiki/Event-driven_architecture).\nYou can copy or fork this project to quickly set up a\nnew project with the same event-driven architecture.\n\n## Event-driven architecture\n\n[aws.amazon.com](https://aws.amazon.com/event-driven-architecture/) -\n[microservices.io](https://microservices.io/patterns/index.html) - \n[wikipedia.org](https://en.wikipedia.org/wiki/Event-driven_architecture)\n\nInstead of the traditional request/response paradigm we achieve with REST APIs,\nevent-driven architecture allows us to use an asynchronous publish/consume pattern.\nThis is a popular way to structure decoupled microservices.\n\nAn event is when state has been updated, and it is relevant for other services.\nThis could for example be **ORDER_CREATED** or **PAYMENT_FAILED**.\nProducers are responsible for publishing events and are unaware of which services \nconsume them or how they process the events. Producers will simply just publish the event and move on.\nConsumers are responsible for consuming events and can then process \nthem which will likely lead to new events being published from the same service.\n\n### Benefits\n\n- **Decoupling**: Producers and consumers do not need to communicate directly so there is no waiting for a response.\nServices can evolve independently without the risk of introducing system-wide issues.\n- **Scalability**: Microservices are loosely coupled so it is easier to scale without affecting other parts of the system.\n- **Flexibility**: It is easier to add new functionality without affecting other services.\nYou can even use different technologies for separate microservices.\n- **Performance**: Events can be processed in parallel by multiple consumers.\n\n### Obstacles\n\n- **At-least-once delivery**: Events can be delivered more than once, so consumer methods must be idempotent.\n- **Ordering**: Event order is not guaranteed, so consumers must be able to handle events out of order.\nHowever, you can set up strict ordering such as FIFO but this may reduce performance.\n- **Schema evolution**: Events can change over time, so backward compatibility must be ensured.\n\n## Project structure\n\nThis sample project consists of two independently runnable microservices named **order** and **payment**.\nEach service has a database and is responsible for its own domain.\nBoth services will consume and publish relevant events.\nThe **order** service uses \n[Spring Boot Web MVC](https://github.com/spring-projects/spring-boot)\nto expose a REST API which has an endpoint for creating a new order.\nSending a request to this endpoint will initiate the microservices.\n\nThe **order** service will create an order and publish an **OrderCreatedEvent**.\nThis event is then consumed by the **payment** service.\nFor demonstration purposes, the **payment** service will then randomly\ndecide whether the payment completed or failed.\nBelow you can see a table presenting the two possible flows.\n\n| API (sync)         | Input events     | Service | Action           | Output events    |\n|--------------------|------------------|---------|------------------|------------------|\n| POST /order/create |                  | Order   | Create order     | OrderCreated     |\n|                    | OrderCreated     | Payment | Complete payment | PaymentCompleted |\n|                    | PaymentCompleted | Order   | Complete order   | OrderCompleted   |\n| -                  |                  |         |                  |                  |\n| POST /order/create |                  | Order   | Create order     | OrderCreated     |\n|                    | OrderCreated     | Payment | Fail payment     | PaymentFailed    |\n|                    | PaymentFailed    | Order   | Cancel order     | OrderCancelled   |\n\n## Patterns\n\n### Choreography-based saga\n[aws.amazon.com](https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/saga-choreography.html) - \n[microservices.io](https://microservices.io/patterns/data/saga.html)\n\nThe **order** and **payment** service are part of a **choreography-based saga**.\nEach service is responsible for its own domain, and they communicate through events.\nOnce an event is published, the producer does not know how another service might \nconsume and process it. Services simply react to consumed events and publish new events.\nAn important thing about this pattern is that every action needs a compensating action \nif something fails. For example, if the **payment** service fails to complete a payment, \nit must publish a **PaymentFailedEvent**. The **order** service will then consume \nthis event and cancel the order. This is a way to ensure eventual consistency.\n\nIf we had a central orchestrator, it would be called an **orchestration-based saga**.\n\n### Transactional outbox\n[aws.amazon.com](https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html) -\n[microservices.io](https://microservices.io/patterns/data/transactional-outbox.html)\n\nA common issue with event-driven architecture is ensuring that state has been saved\nto the database before an event is published. If we try to save an entity to the database\nand publish an event at the same time, it can lead to issues as the database transaction\nmay not have been committed before the event is published.\n\nExample issue:\n1. **Order** service tries to save an order to the database.\n   - Transaction has not been committed yet.\n2. **Order** service publishes an **OrderCreatedEvent**.\n3. **Payment** service consumes the **OrderCreatedEvent**.\n4. **Payment** service publishes a **PaymentCompletedEvent**.\n5. **Order** service consumes the **PaymentCompletedEvent**.\n6. **Order** service tries to complete the order.\n   - Transaction has still not been committed yet.\n   - Order cannot be completed because it does not exist in the database.\n\nThis is solved by using the **transactional outbox** pattern in the two microservices.\nWe must ensure that an event is not published before the state has been saved to the database. \nWhen an order is created, the **order** service will save the order to the database,\nbut it will also save an **OrderCreatedEvent** to the outbox table.\nThen a separate scheduled task will poll the outbox table and publish the event.\nThis ensures the database transaction has been committed before the event is published.\nEssentially, an event can never be published before a specific database\ntransaction has been committed.\n\n### Idempotency and deduplication\n[microservices.io](https://microservices.io/patterns/communication-style/idempotent-consumer.html)\n\nConsidering that many messaging services guarantee **at-least-once delivery**, \nwe must ensure that our services can gracefully handle duplicate events.\nIdeally, our methods should always be idempotent which means that calling\na method multiple times with the same input always produces the same output.\nIf your methods are idempotent, then you can safely process the same event multiple times.\n\nSometimes it is not possible to make a method idempotent.\nIn that case, we can for example use some type of deduplication mechanism to\ngracefully handle duplicate events. This project showcases an example of how\nyou can implement deduplication. \n\nIn the **deduplication** subproject, you can find \n[ProcessedEventRepo](modules/deduplication/src/main/java/com/github/thorlauridsen/deduplication/ProcessedEventRepo.java) \nand [ProcessedEventEntity](modules/deduplication/src/main/java/com/github/thorlauridsen/deduplication/ProcessedEventEntity.java).\nThese are used to store processed events in the database.\nWhen a service consumes an event, it will first check if the event has already been processed.\nIf the event has already been processed, then the service will not process the event again.\nIf the event has not been processed, then the service will process the event and mark \nthe event as processed.\n\n### Database per service\n[aws.amazon.com](https://docs.aws.amazon.com/prescriptive-guidance/latest/modernization-data-persistence/database-per-service.html) -\n[microservices.io](https://microservices.io/patterns/data/database-per-service.html)\n\nEach service has its own database which is a common pattern in event-driven architecture.\nThis allows each service to have its own database schema related to its own domain.\nA benefit here is that multiple services do not need to rely and depend on the same \nshared database schema. This allows for more scalability and independence.\nA specific service could even use a completely different database technology than another service.\n\n## Usage\n\n### Docker Compose\nTo run the system with [Docker Compose](https://docs.docker.com/compose/),\nclone the project to your local machine, go to the root directory and use:\n```\ndocker-compose up -d\n```\nThis will launch the entire project with \n[LocalStack](https://github.com/localstack/localstack), \n[PostgreSQL](https://www.postgresql.org/) \nand the two microservices.\n\n### Gradle\n\nFor this project I have decided to create an independent SQS queue and SNS topic for each event.\nYou can use\n[LocalStack](https://github.com/localstack/localstack)\nto run AWS services locally through\n[Docker](https://www.docker.com/).\nIf you wish to run the microservices in this project,\nyou must first start LocalStack and create the queues and topics.\n\nOnce you have set up **localstack** and **awslocal**, open a terminal and use:\n```\nlocalstack start -d\n```\n\nThen you can create the queues and topics with the following commands:\n```\nawslocal sqs create-queue --queue-name order-created-queue\nawslocal sns create-topic --name order-created-topic\nawslocal sqs get-queue-attributes --queue-url http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/order-created-queue --attribute-name QueueArn\nawslocal sns subscribe --topic-arn arn:aws:sns:us-east-1:000000000000:order-created-topic --protocol sqs --notification-endpoint arn:aws:sqs:us-east-1:000000000000:order-created-queue\nawslocal sqs create-queue --queue-name payment-completed-queue\nawslocal sns create-topic --name payment-completed-topic\nawslocal sqs get-queue-attributes --queue-url http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/payment-completed-queue --attribute-name QueueArn\nawslocal sns subscribe --topic-arn arn:aws:sns:us-east-1:000000000000:payment-completed-topic --protocol sqs --notification-endpoint arn:aws:sqs:us-east-1:000000000000:payment-completed-queue\nawslocal sqs create-queue --queue-name payment-failed-queue\nawslocal sns create-topic --name payment-failed-topic\nawslocal sqs get-queue-attributes --queue-url http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/payment-failed-queue --attribute-name QueueArn\nawslocal sns subscribe --topic-arn arn:aws:sns:us-east-1:000000000000:payment-failed-topic --protocol sqs --notification-endpoint arn:aws:sqs:us-east-1:000000000000:payment-failed-queue\n```\n\nClone the project to your local machine, go to the root directory and use\nthese two commands in separate terminals.\n```\n./gradlew order:bootRun\n```\n```\n./gradlew payment:bootRun\n```\nThis will start the two microservices each using an in-memory H2 database.\nYou can also use IntelliJ IDEA to easily run the two services at once.\n\n### Swagger Documentation\nOnce the entire system is running, you can view the Swagger documentation at:\n- http://localhost:8080/ for the **order** service\n- http://localhost:8081/ for the **payment** service\n\n## Technology\n\n- [JDK25](https://openjdk.org/projects/jdk/25/) - Latest JDK with long-term support\n- [Gradle](https://github.com/gradle/gradle) - Used for compilation, building, testing and dependency management\n- [Spring Cloud AWS](https://github.com/awspring/spring-cloud-aws) - For interacting with Amazon Web Services SQS and SNS\n- [LocalStack](https://github.com/localstack/localstack) - For testing Amazon Web Services SQS and SNS locally\n- [Docker](https://www.docker.com/) - Used to run LocalStack in a Docker container\n- [Springdoc](https://github.com/springdoc/springdoc-openapi) - Provides Swagger documentation for REST APIs\n- [Spring Boot Web MVC](https://github.com/spring-projects/spring-boot) - For creating REST APIs\n- [Spring Data JPA](https://docs.spring.io/spring-data/jpa/reference/index.html) - Repository support for JPA\n- [PostgreSQL](https://www.postgresql.org/) - Open-source relational database\n- [H2database](https://github.com/h2database/h2database) - Provides an in-memory database for simple local testing\n- [Liquibase](https://github.com/liquibase/liquibase) - Used to manage database schema changelogs\n\n## Gradle best practices\n[docs.gradle.org](https://docs.gradle.org/current/userguide/performance.html) - [kotlinlang.org](https://kotlinlang.org/docs/gradle-best-practices.html)\n\n### Preface\nThis project uses Java but the linked article above is generally meant \nfor Kotlin projects. However, I still think that the recommended best \npractices for Gradle are relevant for a Java project as well. \nThe recommendations can be useful for all sorts of Gradle projects.\n\n### ✅ Use Kotlin DSL\nThis project uses Kotlin DSL instead of the traditional Groovy DSL by\nusing **build.gradle.kts** files instead of **build.gradle** files.\nThis gives us the benefits of strict typing which lets IDEs provide\nbetter support for refactoring and auto-completion.\nIf you want to read more about the benefits of using\nKotlin DSL over Groovy DSL, you can check out\n[gradle-kotlin-dsl-vs-groovy-dsl](https://github.com/thorlauridsen/gradle-kotlin-dsl-vs-groovy-dsl)\n\n### ✅ Use a version catalog\n\nThis project uses a version catalog\n[local.versions.toml](gradle/local.versions.toml)\nwhich allows us to centralize dependency management.\nWe can define versions, libraries, bundles and plugins here.\nThis enables us to use Gradle dependencies consistently across the entire project.\n\nDependencies can then be implemented in a specific **build.gradle.kts** file as such:\n```kotlin\nimplementation(local.spring.boot.starter)\n```\n\nThe Kotlinlang article says to name the version catalog **libs.versions.toml**\nbut for this project it has been named **local.versions.toml**. The reason\nfor this is that we can create a shared common version catalog which can\nbe used across Gradle projects. Imagine that you are working on multiple\nsimilar Gradle projects with different purposes, but each project has some\nspecific dependencies but also some dependencies in common. The dependencies\nthat are common across projects could be placed in the shared version catalog\nwhile specific dependencies are placed in the local version catalog.\n\n### ✅ Use local build cache\n\nThis project uses a local\n[build cache](https://docs.gradle.org/current/userguide/build_cache.html)\nfor Gradle which is a way to increase build performance because it will\nre-use outputs produced by previous builds. It will store build outputs\nlocally and allow subsequent builds to fetch these outputs from the cache\nwhen it knows that the inputs have not changed.\nThis means we can save time building\n\nGradle build cache is disabled by default so it has been enabled for this\nproject by updating the root [gradle.properties](gradle.properties) file:\n```properties\norg.gradle.caching=true\n```\n\nThis is enough to enable the local build cache\nand by default, this will use a directory in the Gradle User Home\nto store build cache artifacts.\n\n### ✅ Use configuration cache\n\nThis project uses\n[Gradle configuration cache](https://docs.gradle.org/current/userguide/configuration_cache.html)\nand this will improve build performance by caching the result of the\nconfiguration phase and reusing this for subsequent builds. This means\nthat Gradle tasks can be executed faster if nothing has been changed\nthat affects the build configuration. If you update a **build.gradle.kts**\nfile, the build configuration has been affected.\n\nThis is not enabled by default, so it is enabled by defining this in\nthe root [gradle.properties](gradle.properties) file:\n```properties\norg.gradle.configuration-cache=true\norg.gradle.configuration-cache.parallel=true\n```\n\n### ✅ Use modularization\n\nThis project uses modularization to create a\n[multi-project Gradle build](https://docs.gradle.org/current/userguide/multi_project_builds.html).\nThe benefit here is that we optimize build performance and structure our\nentire project in a meaningful way. This is more scalable as it is easier\nto grow a large project when you structure the code like this.\n\n```\nroot\n│─ build.gradle.kts\n│─ settings.gradle.kts\n│─ apps\n│   └─ order\n│       └─ build.gradle.kts\n│   └─ payment\n│       └─ build.gradle.kts\n│─ modules\n│   ├─ consumer\n│   │   └─ build.gradle.kts\n│   ├─ deduplication\n│   │   └─ build.gradle.kts\n│   ├─ event\n│   │   └─ build.gradle.kts\n│   ├─ exception\n│   │   └─ build.gradle.kts\n│   ├─ model\n│   │   └─ build.gradle.kts\n│   ├─ outbox\n│   │   └─ build.gradle.kts\n│   └─ producer\n│       └─ build.gradle.kts\n```\n\nThis also allows us to specifically decide which Gradle dependencies will be used\nfor which subproject. Each subproject should only use exactly the dependencies\nthat they need.\n\nSubprojects located under [apps](apps) are runnable, so this means we can\nrun the **order** project to spin up a service. We can add more\nsubprojects under [apps](apps) to create additional runnable microservices.\n\nSubprojects located under [modules](modules) are not independently runnable.\nThe subprojects are used to structure code into various layers. The **model**\nsubproject is the most inner layer and contains domain model classes and this\nsubproject knows nothing about any of the other subprojects. The purpose of\nthe **deduplication** subproject is to provide functionality for idempotency and \ndeduplication. We can add more non-runnable subprojects under [modules](modules) \nif necessary.\n\n---\n\n#### Subproject with other subproject as dependency\n\nThe subprojects in this repository may use other subprojects as dependencies.\n\nIn our root [settings.gradle.kts](settings.gradle.kts) we have added:\n```kotlin\nenableFeaturePreview(\"TYPESAFE_PROJECT_ACCESSORS\")\n```\nWhich allows us to add a subproject as a dependency in another subproject:\n\n```kotlin\ndependencies {\n    implementation(projects.model)\n}\n```\n\nThis essentially allows us to define this structure:\n\n```\norder\n│─ consumer\n│─ event\n│─ exception\n│─ model\n│─ outbox\n└─ producer\n\npayment\n│─ consumer\n│─ event\n│─ exception\n│─ model\n│─ outbox\n└─ producer\n\nconsumer\n│─ event\n└─ model\n\ndeduplication\n└─ model\n\nevent\n└─ model\n\noutbox\n└─ model\n\nproducer\n│─ event\n└─ model\n\nexception and model has no dependencies\n```\n\n## Meta\n\nThis project has been created with the sample code structure from:\n[thorlauridsen/spring-boot-java-sample](https://github.com/thorlauridsen/spring-boot-java-sample).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthorlauridsen%2Fspring-cloud-java-event-driven","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthorlauridsen%2Fspring-cloud-java-event-driven","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthorlauridsen%2Fspring-cloud-java-event-driven/lists"}