Start Minikube with sufficient resources.
minikube start --memory=4096 --cpus=2
Use kubectl to apply each of these YAML files. This will create the necessary Kubernetes resources.
kubectl apply -f redis.yml
kubectl apply -f pos_postgres_write.yml
kubectl apply -f pos_postgres_read.yml
kubectl apply -f pos_debezium.yml
kubectl apply -f pos_service.yml
Check the status of your pods and services to ensure they are running correctly.
kubectl get pods
kubectl get services
Minikube provides a way to access services running inside the cluster using minikube service.
minikube service pos-service
You can also use kubectl port-forward
to forward a port from your local machine to a port on a pod. For example:
kubectl port-forward deployment/pos-service 8080:8080
We provided a OpenAPI documentation for the PlayOfferService.
It can be found in the openapi.json
file.
All .cs
files are linked to the respective file in the project
The Saga pattern is used to maintain data consistency in a microservice architecture. It is a sequence of local transactions where each transaction updates the data and publishes a message or event to trigger the next transaction in the sequence. If a transaction fails, the saga executes a series of compensating transactions that undo the changes that were made by the preceding transactions.
In the PlayOfferService the Saga Pattern is used in conjunction with the CourtService, to automatically create a Reservation if a PlayOffer is joined by a second Member. It includes the following Steps:
- After
JoinPlayOfferCommand
is received, the PlayOfferService publishes aPlayOfferJoinedEvent
- The CourtService listens to the
PlayOfferJoinedEvent
and tries to create a Reservation for the PlayOffer at the specified time in the PlayOfferJoinedEvent - The CourtService publishes one of three possible events, each containing the
EventId
of thePlayOfferJoinedEvent
in theCorrelationId
:ReservationCreatedEvent
if the Reservation was successfully createdReservationRejectedEvent
if the Reservation could not be created (e.g. no court available)ReservationLimitExceededEvent
if the Reservation could not be created due to a limit of Reservations per Member
- The PlayOfferService listens to the events published by the CourtService and reacts depending on the event:
- If a
ReservationCreatedEvent
is received it then triggers aPlayOfferReservationAddedEvent
in the PlayOfferService to add the Reservation to the respective PlayOffer - If a
ReservationRejectedEvent
orReservationLimitExceededEvent
is received it then triggers aPlayOfferOpponentRemovedEvent
to revert the changes of thePlayOfferJoinedEvent
- If a
The compensation logic for the Saga is implemented in the ReservationEventHandler File in the functions in lines 81 - 102.
The CQRS pattern is used in the PlayOfferService to separate the read and write operations for PlayOffers. The write operations are implemented using commands, which are located in the Commands folder. The read operations are implemented using queries, which are located in the Queries folder. Each query and command is then handled by their respective handlers, which are located in the root of the Handlers folder. Each handler is responsible for executing the logic for a specific command or query.
The following queries are implemented in the PlayOfferService with their respective handlers:
GetPlayOffersByClubIdQuery.cs
(Line 1:8): Returns all PlayOffers for a specific Club. The query is created and sent to the handler in thePlayOfferController.cs
(Line 40).GetPlayOffersByClubIdHandler.cs
(Line 1:49): Handles theGetPlayOffersByClubIdQuery
and returns the PlayOffers for the specified Club
GetPlayOffersByParticipantIdQuery.cs
(Line 1:8): Returns all PlayOffers for a specific participant (either as creator or opponent). The query is created and sent to the handler in thePlayOfferController.cs
(Line 64).GetPlayOffersByParticipantIdHandler.cs
(Line 1:49): Handles theGetPlayOffersByParticipantIdQuery
and returns the PlayOffers for the specified participant
GetPlayOffersByCreatorNameQuery.cs
(Line 1:8): Returns a specific PlayOffer by the name of it's creator. The query is created and sent to the handler in thePlayOfferController.cs
(Line 91).GetPlayOffersByCreatorNameHandler.cs
(Line 1:59): Handles theGetPlayOfferByCreatorNameQuery
and returns the PlayOffer with the specified Id
The following commands are implemented in the PlayOfferService with their respective handlers:
CancelPlayOfferCommand.cs
(Line 1:7): Cancels a PlayOffer. The command is created and sent to the handler in thePlayOfferController.cs
(Line 158).CancelPlayOfferHandler.cs
(Line 1:79): Handles theCancelPlayOfferCommand
and cancels the PlayOffer
CreatePlayOfferCommand.cs
(Line 1:7): Creates a new PlayOffer. The command is created and sent to the handler in thePlayOfferController.cs
(Line 128).CreatePlayOfferHandler.cs
(Line 1:87): Handles theCreatePlayOfferCommand
and creates a new
JoinPlayOfferCommand.cs
(Line 1:7): Joins a PlayOffer. The command is created and sent to the handler in thePlayOfferController.cs
(Line 192).JoinPlayOfferHandler.cs
(Line 1:100): Handles theJoinPlayOfferCommand
and joins the PlayOffer
In the PlayOfferService, projections are implemented using the Mediator Pattern which is implemented, in dedicated EventHandlers
for each entity, in the Events folder.
Each Entity has a dedicated RedisStreamReader
which subscribes to the Redis stream and listens to, filters and parses the events for a specific entity:
PlayOfferEventHandler.cs
(Line 1:94): Handles the events for thePlayOffer
entityMemberEventHandler.cs
(Line 1:126): Handles the events for theMember
entityReservationEventHandler.cs
(Line 1:151): Handles the events for theReservation
entityCourtEventHandler.cs
(Line 1:57): Handles the events for theCourt
entityClubEventHandler.cs
(Line 1:116): Handles the events for theClub
entity
The EventHandlers
receive their events from the RedisStreamService
and then apply the events to the respective entity:
RedisClubStreamService.cs
(Line 1:82): Read the events from the redis club stream and sends them to theClubEventHandler
RedisCourtStreamService.cs
(Line 1:83): Read the events from the redis court stream and sends them to theCourtEventHandler
RedisMemberStreamService.cs
(Line 1:86): Read the events from the redis member stream and sends them to theMemberEventHandler
RedisPlayOfferStreamService.cs
(Line 1:68): Read the events from the redis play offer stream and sends them to thePlayOfferEventHandler
RedisReservationStreamService.cs
(Line 1:76): Read the events from the redis reservation stream and sends them to theReservationEventHandler
The write side of the CQRS implementation is using a event sourcing pattern. In the PlayOfferService, events are used to represent changes to the state of Entities. When a command is received, it is validated and then converted into one or more events, which are then stored in the write side database.
The events are structured with a hierarchy of event classes:
Technical[...]Event
: Represents a group of events that are used for a specific entity, these are used to route the events to the correctEventHandler
in the read model. Implements theBaseEvent
class.TechnicalPlayOfferEvent.cs
(Line 1:7): Represents the events for thePlayOffer
entityTechnicalMemberEvent.cs
(Line 1:7): Represents the events for theMember
entityTechnicalReservationEvent.cs
(Line 1:8): Represents the events for theReservation
entityTechnicalCourtEvent.cs
(Line 1:7): Represents the events for theCourt
entityTechnicalClubEvent.cs
(Line 1:7): Represents the events for theClub
entity
BaseEvent.cs
(Line 1:34): Represents the whole event including the following metadata: Each event class represents a specific type of event that can occur in the system.event_id
: The unique identifier for the evententity_id
: The unique identifier for the entity that the event belongs toevent_type
: The type of the evententity_type
: The type of the entity that the event belongs totimestamp
: The timestamp when the event occurredcorrelation_id
: The correlation id of the event
DomainEvent.cs
(Line 1:35): Is used as the data type of theeventData
property in theBaseEvent
class. It is also used for json serialization and deserialization.
The smallest unit of events can be found in the Events folder. Each event class represents a specific type of event that can occur in the system and implements the DomainEvent
class.
-
PlayOfferEvents:
PlayOfferCreatedEvent.cs
(Line 1:26): Represents the event when a PlayOffer is createdPlayOfferJoinedEvent.cs
(Line 1:9): Represents the event when a Opponent joins a PlayOfferPlayOfferCancelledEvent.cs
(Line 1:6): Represents the event when a PlayOffer is canceledPlayOfferReservationAddedEvent.cs
(Line 1:6): Represents the event when a Reservation was created by the court service and was now added to the PlayOfferPlayOfferOpponentRemovedEvent.cs
(Line 1:5): Represents the event when no Reservation could be created by the court service and therefore the opponent was removed from the PlayOffer
-
MemberEvents:
MemberCreatedEvent.cs
(Line 1:13): Represents the event when a Member is createdMemberDeletedEvent.cs
(Line 1:5): Represents the event when a Member is deletedMemberEmailChangedEvent.cs
(Line 1:6): Represents the event when the email of a Member is changedMemberFullNameChangedEvent.cs
(Line 1:8): Represents the event when the name of a Member is changedMemberLockedEvent.cs
(Line 1:6): Represents the event when a Member is lockedMemberUnlockedEvent.cs
(Line 1:6): Represents the event when a Member is unlocked
-
ReservationEvents:
ReservationCreatedEvent.cs
(Line 1:21): Represents the event when a Reservation is createdReservationCancelledEvent.cs
(Line 1:5): Represents the event when a Reservation is canceledReservationLimitExceededEvent.cs
(Line 1:21): Represents the event when the limit of Reservations per Member is exceededReservationRejectedEvent.cs
(Line 1:6): Represents the event when a Reservation could not be created
-
CourtEvents:
CourtCreatedEvent.cs
(Line 1:14): Represents the event when a Court is createdCourtUpdatedEvent.cs
(Line 1:12): Represents the event when a Court is changed
-
ClubEvents:
ClubCreatedEvent.cs
(Line 1:13): Represents the event when a Club is createdClubDeletedEvent.cs
(Line 1:16): Represents the event when a Club is deletedClubNameChangedEvent.cs
(Line 1:6): Represents the event when the name of a Club is changedClubLockedEvent.cs
(Line 1:6): Represents the event when a Club is lockedClubUnlockedEvent.cs
(Line 1:6): Represents the event when a Club is unlocked
The events are applied to the entities in the apply
methods, the implementation location can be found under Domain Driven Design.
In the PlayOfferService, the idempotency of all events is guaranteed!
All events which were read from the redis stream and were processed by the EventHandlers
are saved into the AppliedEvents
table in the read side database. This allows us to check if a received event was already processed and therefore can be ignored.
Therefore the outcome of all events won't change if they are processed multiple times.
In the PlayOfferService, Authentication and Authorization are implemented using a JWT token, which is is provided by the club service. All requests to the PlayOfferService must include a valid JWT token in the Authorization header.
All Queries can be executed by users with the ADMIN
and MEMBER
role. The commands can only be executed by users with the MEMBER
roles.
A custom JwtClaimsMiddleware.cs
(Line 1:43) is used to extract the claims from the JWT token and add them to the HttpContext
of the request.
These claims are then checked with the Authorize
attribute in the PlayOfferController.cs
(Lines 31,55,80,115,147,181) to ensure that the user has the necessary roles to execute the request.
Furthermore, most requests also extract the memberId
and/or the clubId
from the claims to ensure that the user can only access their own data, this can be seen in PlayOfferController.cs
(Lines 39,63,122:123,154,189).
In the PlayOfferService, Optimistic Locking is implemented using the EFCore
and its transaction mechanism. When a request is received, the current amount of events is read and incremented by one.
When the request is processed, the amount of events is read again and compared to the initial amount. If the amount of events has changed unexpectedly during the transaction, a concurrency exception is thrown and the transaction rolled back.
Otherwise the transaction is committed and the changes are saved to the database.
The Optimistic Locking is implemented in the each CommandHandler in the Commands folder.
CancelPlayOfferHandler.cs
- Line 26:27
- Line 67:75
JoinPlayOfferHandler.cs
- Line 29:30
- Line 88:96
CreatePlayOfferHandler.cs
- Line 69:70
- Line 75:83
In the PlayOfferService, DDD is used to model the core domain of the application, which includes the following entities:
PlayOffer.cs
(Line 1:81): Represents a play offer that is created by a member and can be joined by other membersMember.cs
(Line 1:81): Represents a member of the platform who can create and join play offersReservation.cs
(Line 1:51): Represents a reservation for a play offer that is created by the court serviceCourt.cs
(Line 1:45): Represents a court that can be reserved for a play offerClub.cs
(Line 1:66): Represents a club that can have multiple courts and members
Since event sourcing was also used each entity implements a apply
method which is used to apply the events to the entity. It is important to note that the apply
method is not allowed to fail, as it is used to reconstruct the state of the entity and the correctness of the events is guaranteed by the CommandHandlers
.
The implementation for the apply
methods can be found here:
PlayOffer.cs
(Line 23)Member.cs
(Line 17)Reservation.cs
(Line 18)Court.cs
(Line 14)Club.cs
(Line 14)
However, we didn't implement a process
method in each entity, since the processing of the events is done in the CommandHandlers
.
In the PlayOfferService, Transaction Log Trailing is implemented using Debezium, which is an open-source platform for change data capture. Debezium captures changes to the PostgreSQL database and publishes them to a Redis Stream.
The Debezium configuration can be found in the pos_debezium.yml(Line 1:21) file.