Spring Food Delivery Microservices
is a fictional food delivery microservices, built with .Net Core and different software architecture and technologies like Microservices Architecture, Vertical Slice Architecture , CQRS Pattern, Domain Driven Design (DDD), Event Driven Architecture. For communication between independent services, we use asynchronous messaging with using rabbitmq on top ofspring amqp
, and sometimes we use synchronous communication for real-time communications with using REST calls.
Note
This application is not business oriented
and my focus is mostly on technical part, I just want to implement a sample with using different technologies, software architecture design, principles and all the thing we need for creating a microservices app.
Warning
This project is in progress. I add new features over the time. You can check the Release Notes.
Important
Other versions of this project like golang and .net are available in these repositories:
If you like feel free to ⭐ this repository, It helps out :)
Thanks a bunch for supporting me!
- ✅ Using Spring MVC as a Web Framework.
- ✅ Using
Microservices
andVertical Slice Architecture
as a high level architecture - ✅ Using
Event Driven Architecture
on top of RabbitMQ Message Broker andSpring AMQP
library - ✅ Using
Domain Driven Design
in most of services like Customers, Catalogs, ... - ✅ Using
CQRS Pattern
on top ofjava-mediator
package and splittingread models
andwrite models
- ✅ Using
Structured logging
withslf4j
andlog4j2
and usingConsole Appender
to write logs to the console andOpenTelemetryAppender
to send logs tografana loki
andkibana
- ✅ Using
Outbox Pattern
for all microservices for Guaranteed Delivery or At-least-once Delivery - ✅ Using
Inbox Pattern
for handling Idempotency in reciver side and Exactly-once Delivery - ✅ Using
Fluent Validation
andSpring Validation
and Validation Pipeline Behavior on top ofjava-mediator
package - ✅ Using
Postgres
on top ofSpring Data JPA
for write database as relational DB andMongoDB
on top ofSpring Data MongoDB
for read database - ✅ Using
docker
anddocker-compose
for deployment - ✅ Using Spring Cloud Gateway as reverse proxy and API Gateway
- ✅ Using different type of tests like
Unit Tests
,Integration Tests
,End-To-End Tests
and testcontainers for testing in isolation - ✅ Using OpenTelemetry Spring Boot starter which collects some packages from open-telemetry/opentelemetry-java-instrumentation/ and open-telemetry/opentelemetry-java to collect
Logs
,Metrics
andTraces
and sending telemetry data to different exporters like prometheus, tempo, loki, jaeger, kibana through opentelemetry collector and showing them in grafana dashboard - ✅ Using
Unit Testing
for testing small units and mocking our dependencies withMockito
. - ✅ Using
End-To-End Testing
andIntegration Testing
for testing features with all dependencies usingtestcontainers
. - ✅ Using Springdoc Openapi for generating OpenAPI documentation in Spring Boot.
This project is in progress, new features will be added over time.
Feature | Architecture Pattern | Status | CI-CD |
---|---|---|---|
Building Blocks | - | ✅ Completed | - |
API Gateway | - | ✅ Completed | - |
Catalogs Service | Domain Driven Design | ✅ Completed | - |
Identity Service | Using Keycloak | ✅ Completed | - |
Users Service | Data Centric Design | 👷 In-Progress | - |
Customers Service | Domain Driven Design | 👷 In-Progress | - |
Orders Service | Event Sourcing | ❌ Not Started | - |
- ✔️ Spring Boot - Framework for building Java applications with pre-configured defaults and embedded server support.
- ✔️ Spring-Cloud-Gateway - An API Gateway built on Spring Framework and Spring Boot providing routing and more.
- ✔️ Spring AMQP - Simplifies messaging using RabbitMQ with declarative configuration and templates.
- ✔️ Spring Data JPA - Enhances JPA with repository abstractions and advanced query capabilities.
- ✔️ Spring Data MongoDB - Provides seamless MongoDB integration with Spring-based applications.
- ✔️ Spring Security - Comprehensive security framework for authentication and authorization in Java applications.
- ✔️ Springdoc OpenAPI - Automatically generates OpenAPI 3 documentation for Spring Boot projects.
- ✔️ Swagger Core - Core library for building and consuming Swagger-compliant APIs.
- ✔️ Opentelemetry-Java - OpenTelemetry Java SDK and using its BOM.
- ✔️ Opentelemetry-Java-Instrumentation - OpenTelemetry auto-instrumentation and instrumentation libraries for Java and using its BOM.
- ✔️ OpenTelemetry Collector - Vendor-agnostic way to receive, process and export telemetry data.
- ✔️ Flyway - Database migration tool for version-controlled and repeatable schema changes.
- ✔️ JPA Buddy - Productivity tool for working with JPA and Hibernate, simplifying development and debugging.
- ✔️ ULID Creator - Library for generating UUIDs in various formats and versions.
- ✔️ QueryDSL - Enables type-safe queries for JPA, SQL, and other persistence layers.
- ✔️ Testcontainers - Provides lightweight, disposable Docker containers for testing purposes.
- ✔️ Mockito - Popular mocking framework for writing clean, maintainable unit tests in Java.
- ✔️ JUnit - Essential testing framework for Java developers, supporting unit and integration testing.
- ✔️ Rest-Assured - Java DSL for easy testing of REST services.
- ✔️ Spotbugs - SpotBugs is FindBugs' successor. A tool for static analysis to look for bugs in Java code.
- ✔️ Mapstruct - An annotation processor for generating type-safe bean mappers.
- ✔️ Wiremock - A tool for mocking HTTP services.
- ✔️ Datafaker - Generating fake data for the JVM (Java, Kotlin, Groovy) has never been easier!
- ✔️ Guava - Google core libraries for Java.
- ✔️ Hibernate-Metamodel-Generator - Annotation Processor to generate JPA 2 static metamodel classes.
- ✔️ Spotless - Keep your code spotless.
- ✔️ Palantir-Java-Format - A modern, lambda-friendly, 120 character Java formatter.
TODO
The bellow architecture shows that there is one public API (API Gateway) which is accessible for the clients and this is done via HTTP request/response. The API gateway then routes the HTTP request to the corresponding microservice. The HTTP request is received by the microservice that hosts its own REST API. Each microservice is running within its own AppDomain
and has directly access to its own dependencies such as databases, files, local transaction, etc. All these dependencies are only accessible for that microservice and not to the outside world. In fact microservices are decoupled from each other and are autonomous. This also means that the microservice does not rely on other parts in the system and can run independently of other services.
Microservices are event based which means they can publish and/or subscribe to any events occurring in the setup. By using this approach for communicating between services, each microservice does not need to know about the other services or handle errors occurred in other microservices.
In this architecture we use CQRS Pattern for separating read and write model beside of other CQRS Advantages. Here for now I don't use Event Sourcing for simplicity but I will use it in future for syncing read and write side with sending streams and using Projection Feature for some subscribers to syncing their data through sent streams and creating our Custom Read Models in subscribers side.
Here I have a write model that uses a postgres database for handling better Consistency
and ACID Transaction
guaranty. beside o this write side I use a read side model that uses MongoDB for better performance of our read side without any joins with using some nested documents, also better scalability with some good scaling features in MongoDB.
For syncing our read side and write side we have 2 options with using Event Driven Architecture (without using events streams in event sourcing):
-
If our
read sides
are insame service
, during saving data in write side and in the same transaction, we save a Internal Command record in our PersistMessage storage (like something we do in outbox pattern) and after committing write side, our MessagePersistenceBackgroundService and MessagePersistenceServiceImpl reads unsent internal commands and sends them to their correspondinginternal command handlers
in same service and these handlers can save their read models in our MongoDb database as a read side. -
If our
read sides
are inanother services
we publish an integration event (with saving this message in the outbox) after committing our write side, all of oursubscribers
orconsumers
can get these events and save them in their read models (MongoDB).
All of these are optional in a application and we should only use what the service requires, For example, if the service does not need to use DDD because the business logic is very simple, and it is mostly CRUD
we can use data centric
architecture or If our application is not Task based
instead of CQRS and separating read side and write side, we can just use a simple CRUD
based approach.
Here I used Outbox for Guaranteed Delivery and can be used as a landing zone for integration events before they are published to the message broker.
Outbox pattern ensures that a message was sent (e.g. to a queue) successfully at least once. With this pattern, instead of directly publishing a message to the queue, we put it in the temporary storage (e.g. database table) for preventing missing any message and some retry mechanism in any failure (At-least-once Delivery). For example When we save data as part of one transaction in our service, we also save messages (Integration Events) that we want to process later in another microservices. The list of messages to be processed is called a PersistMessage with
a MessageDeliveryType which can be Outbox
, Inbox
and InternalCommand
and a MessageStatus which can be Stored
and Delivered
. The MessagePersistenceService service is responsible for doing this message processing internally.
Also we have a background service MessagePersistenceBackgroundService that periodically checks the our PersistMessages in the database and try to send the messages to the broker with using our MessagePersistenceService service. After it gets confirmation of publishing (e.g. ACK from the broker) it marks the message as processed to avoid resending
and duplication
.
However, it is possible that we will not be able to mark the message as processed due to communication error, for example broker
is unavailable
. In this case our MessagePersistenceBackgroundService try to resend the messages that not processed and it is actually At-Least-Once delivery. We can be sure that message will be sent once
, but can be sent multiple times
too! That’s why another name for this approach is Once-Or-More delivery. We should remember this and try to design receivers of our messages as Idempotents, which means:
A message that has the same effect whether it is received once or multiple times. This means that a message can safely be resent without causing any problems even if the receiver receives duplicates of the same message.
For handling Idempotency and Exactly-once Delivery in consumers side, we can use Inbox Pattern.
This pattern is similar to Outbox Pattern. It’s used to handle incoming messages (e.g. from a queue) for unique processing
of a single message
only once
(even with executing multiple time). Accordingly, we have a table in which we’re storing incoming messages. Contrary to outbox pattern, we first save the messages in the database, then we’re returning ACK to queue. If save succeeded, but we didn’t return ACK to queue, then delivery will be retried. That’s why we have at-least-once delivery again. After that, an inbox background process
runs and will process the inbox messages that not processed yet. also we can prevent executing a message with specific MessgaeId
multiple times. after executing our inbox message for example with calling our subscribed event handlers we send a ACK to the queue when they succeeded. (Inbox part of the system is in progress, I will cover this part soon as possible)
Also here I used RabbitMQ
as my Message Broker
for my async communication between the microservices with using eventually consistency mechanism, for now I used Spring AMQP for handling event driven communications. beside of using eventually consistency through event driven architecture we have a synchronous calls with using REST
when we need immediate consistency.
I used Spring Cloud Gateway which is an API Gateway
built on top of the Spring as reverse proxy (we can use envoy, traefik, nginx, ...), in front of our services. We can also have multiple Api Gateways for reaching BFF pattern
. For example one Gateway for mobile apps, One Gateway for web apps and etc.
With using api Gateway our internal microservices are transparent and user can not access them directly and all requests will serve through this Gateway.
Also we can use gateway for load balancing, authentication and authorization, caching ,...
In this project I used vertical slice architecture or Restructuring to a Vertical Slice Architecture also I used feature folder structure in this project.
- We treat each request as a distinct use case or slice, encapsulating and grouping all concerns from front-end to back.
- When We adding or changing a feature in an application in n-tire architecture, we are typically touching many different "layers" in an application. we are changing the user interface, adding fields to models, modifying validation, and so on. Instead of coupling across a layer, we couple vertically along a slice and each change affects only one slice.
- We
Minimize coupling
between slices
, andmaximize coupling
in a slice
. - With this approach, each of our vertical slices can decide for itself how to best fulfill the request. New features only add code, we're not changing shared code and worrying about side effects. For implementing vertical slice architecture using cqrs pattern is a good match.
Also here I used CQRS for decompose my features to very small parts that makes our application:
- maximize performance, scalability and simplicity.
- adding new feature to this mechanism is very easy without any breaking change in other part of our codes. New features only add code, we're not changing shared code and worrying about side effects.
- easy to maintain and any changes only affect on one command or query (or a slice) and avoid any breaking changes on other parts
- it gives us better separation of concerns and cross cutting concern (with help of MediatR behavior pipelines) in our code instead of a big service class for doing a lot of things.
With using CQRS, our code will be more aligned with SOLID principles, especially with:
- Single Responsibility rule - because logic responsible for a given operation is enclosed in its own type.
- Open-Closed rule - because to add new operation you don’t need to edit any of the existing types, instead you need to add a new file with a new type representing that operation.
Here instead of some Technical Splitting for example a folder or layer for our services
, controllers
and data models
which increase dependencies between our technical splitting and also jump between layers or folders, We cut each business functionality into some vertical slices, and inner each of these slices we have Technical Folders Structure specific to that feature (command, handlers, infrastructure, repository, controllers, data models, ...).
Usually, when we work on a given functionality we need some technical things for example:
- API endpoint (Controller)
- Request Input (Dto)
- Request Output (Dto)
- Some class to handle Request, For example Command and Command Handler or Query and Query Handler
- Data Model
Now we could all of these things beside each other and it decrease jumping and dependencies between some layers or folders.
Keeping such a split works great with CQRS. It segregates our operations and slices the application code vertically instead of horizontally. In Our CQRS pattern each command/query handler is a separate slice. This is where you can reduce coupling between layers. Each handler can be a separated code unit, even copy/pasted. Thanks to that, we can tune down the specific method to not follow general conventions (e.g. use custom SQL query or even different storage). In a traditional layered architecture, when we change the core generic mechanism in one layer, it can impact all methods.
TODO
Run the docker-compose.infrastructure.yaml file in the root of project, for running required infrastructures to run microservices with bellow command:
docker-compose -f ./deployments/docker-compose/docker-compose.infrastructure.yaml up -d
Now we can run our microservices with using mvn
and make
command and some predefined commands in our Makefile:
For running services firstly we should build and install building-blocks
and shared service
modules:
make build-building-blocks
make install-building-blocks
make build-shared
make install-shared
Then we should build and install other microservices:
make build-catalogs
make install-catalogs
make build-users
make install-users
make build-customers
make install-customers
make build-api-gateway
make install-api-gateway
make build-orders
make install-orders
make run-catalogs
make run-users
make run-customers
make run-api-gateway
make test-catalogs
make test-customers
make test-users
make test-all
flyway-migrate-catalogs
flyway-migrate-users
flyway-migrate-customers
Some useful commands with using make
and mvn
:
# Clean all microservices
make clean-all
# check style rules with spotless
make check-spotless
# apply style rules with spotless
make apply-spotless
The application is in development status. You are feel free to submit pull request or create the issue.
- https://github.com/oskardudycz/EventSourcing.NetCore
- https://github.com/oskardudycz/slim-down-your-aggregate
The project is under MIT license.