Skip to content

Commit c6cf94a

Browse files
added readme
1 parent dbd7385 commit c6cf94a

27 files changed

+131
-69
lines changed

.images/diagram.png

120 KB
Loading

README.md

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Transactional Outbox Pattern
2+
3+
An example of the transactional outbox pattern. The project was implemented as part of my blog post [here](), where you
4+
can find more details about the pattern and implementation.
5+
6+
## Project structure
7+
8+
The project consists of the following modules:
9+
1) [domain-events](./domain-events). Domain events shared between different services.
10+
2) [employee-service](./employee-service). Produces domain events reliably using the transactional outbox pattern
11+
and then forwards them to an SNS FIFO topic. Uses employees as an example.
12+
3) [consumer-service](./consumer-service). It reads the published domain events by polling its own SQS FIFO queue. This
13+
could be any service interested in the events published by the `employee-service`.
14+
15+
## Architecture
16+
17+
![alt text](./.images/diagram.png)
18+
19+
* Employee Service inserts, updates or deletes employee entities and inserts generated domain events in the outbox table in a transaction.
20+
* Employee Service polls the outbox table and forwards events to an SNS topic. I assumed that the order of events matters, so
21+
I used an SNS FIFO topic. Also, no two instances can read the outbox table at the same time.
22+
* Each consumer service has its own SQS FIFO.
23+
* SQS FIFO subscribes to the employee SNS FIFO. A filter can be used to only subscribe to specific event types.
24+
* Consumer Service polls its own queue and uses the domain events.
25+
26+
## Running locally
27+
28+
You can run the project locally using docker. The configuration provided matches the
29+
above architecture diagram. I used localstack, so no need for an AWS account to run the project.
30+
31+
You first need to compile and package all modules.
32+
```shell
33+
mvn clean package -Dmaven.test.skip=true
34+
```
35+
36+
Then you can build and start services in docker.
37+
```shell
38+
docker-compose up --build
39+
```
40+
41+
All services should be up and running.
42+
43+
## Testing locally
44+
45+
Now that all services are running locally we can do some manual testing.
46+
47+
We can create an employee.
48+
```shell
49+
curl --header "Content-Type: application/json" \
50+
--request POST \
51+
--data '{ "firstName": "Ioannis", "lastName": "Ioannou", "email": "[email protected]" }' \
52+
http://localhost:8001/employee
53+
```
54+
55+
The `consumer-service-1` logs that it received the domain event `EmployeeCreated`.
56+
57+
Now if we delete the employee using the UUID from the response.
58+
```shell
59+
curl --request DELETE http://localhost:8001/employee/8741749a-43f0-4463-a662-2ee693de749e
60+
```
61+
62+
The `consumer-service-2` logs that it received the domain event `EmployeeDeleted`.
63+
64+
65+
66+
67+
68+
69+
70+

docker-compose.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
version: '2.4'
22
services:
3-
producer-service:
4-
build: ./producer-service
5-
container_name: producer-service
3+
employee-service:
4+
build: ./employee-service
5+
container_name: employee-service
66
ports:
77
- 8001:8080
88
environment:
File renamed without changes.

producer-service/pom.xml renamed to employee-service/pom.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
<modelVersion>4.0.0</modelVersion>
1212

1313
<groupId>me.ioannisioannou</groupId>
14-
<artifactId>producer-service</artifactId>
14+
<artifactId>employee-service</artifactId>
1515
<version>1.0.0</version>
16-
<name>producer-service</name>
16+
<name>employee-service</name>
1717
<description>Produces domain events reliably using the transactional outbox pattern and forwards them to SNS to be consumed by other services</description>
1818

1919
<properties>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package me.ioannisioannou.transactional.outbox.employee;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.scheduling.annotation.EnableScheduling;
6+
7+
@SpringBootApplication
8+
@EnableScheduling
9+
public class EmployeeApplication {
10+
11+
public static void main(String[] args) {
12+
SpringApplication.run(EmployeeApplication.class, args);
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
package me.ioannisioannou.transactional.outbox.producer.controllers;
1+
package me.ioannisioannou.transactional.outbox.employee.controllers;
22

33
import lombok.RequiredArgsConstructor;
4-
import me.ioannisioannou.transactional.outbox.producer.dtos.CreateEmployeeRequest;
5-
import me.ioannisioannou.transactional.outbox.producer.dtos.CreateEmployeeResponse;
6-
import me.ioannisioannou.transactional.outbox.producer.dtos.UpdateEmployeeRequest;
7-
import me.ioannisioannou.transactional.outbox.producer.dtos.UpdateEmployeeResponse;
8-
import me.ioannisioannou.transactional.outbox.producer.entities.Employee;
9-
import me.ioannisioannou.transactional.outbox.producer.mappers.EmployeeMapper;
10-
import me.ioannisioannou.transactional.outbox.producer.services.EmployeeService;
4+
import me.ioannisioannou.transactional.outbox.employee.dtos.CreateEmployeeRequest;
5+
import me.ioannisioannou.transactional.outbox.employee.dtos.CreateEmployeeResponse;
6+
import me.ioannisioannou.transactional.outbox.employee.dtos.UpdateEmployeeRequest;
7+
import me.ioannisioannou.transactional.outbox.employee.dtos.UpdateEmployeeResponse;
8+
import me.ioannisioannou.transactional.outbox.employee.entities.Employee;
9+
import me.ioannisioannou.transactional.outbox.employee.mappers.EmployeeMapper;
10+
import me.ioannisioannou.transactional.outbox.employee.services.EmployeeService;
1111
import org.springframework.web.bind.annotation.*;
1212

1313
import javax.validation.Valid;
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.ioannisioannou.transactional.outbox.producer.dtos;
1+
package me.ioannisioannou.transactional.outbox.employee.dtos;
22

33
import lombok.Data;
44

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.ioannisioannou.transactional.outbox.producer.dtos;
1+
package me.ioannisioannou.transactional.outbox.employee.dtos;
22

33
import lombok.Builder;
44
import lombok.Data;
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.ioannisioannou.transactional.outbox.producer.dtos;
1+
package me.ioannisioannou.transactional.outbox.employee.dtos;
22

33
import lombok.Data;
44

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.ioannisioannou.transactional.outbox.producer.dtos;
1+
package me.ioannisioannou.transactional.outbox.employee.dtos;
22

33
import lombok.Builder;
44
import lombok.Data;
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.ioannisioannou.transactional.outbox.producer.entities;
1+
package me.ioannisioannou.transactional.outbox.employee.entities;
22

33
import lombok.*;
44
import org.hibernate.annotations.GenericGenerator;

producer-service/src/main/java/me/ioannisioannou/transactional/outbox/producer/entities/Outbox.java renamed to employee-service/src/main/java/me/ioannisioannou/transactional/outbox/employee/entities/Outbox.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.ioannisioannou.transactional.outbox.producer.entities;
1+
package me.ioannisioannou.transactional.outbox.employee.entities;
22

33
import com.fasterxml.jackson.databind.JsonNode;
44
import com.vladmihalcea.hibernate.type.json.JsonType;
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.ioannisioannou.transactional.outbox.producer.events;
1+
package me.ioannisioannou.transactional.outbox.employee.events;
22

33
import lombok.Builder;
44
import lombok.Data;
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.ioannisioannou.transactional.outbox.producer.exceptions;
1+
package me.ioannisioannou.transactional.outbox.employee.exceptions;
22

33
import org.springframework.http.HttpStatus;
44
import org.springframework.web.bind.annotation.ResponseStatus;
+7-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
package me.ioannisioannou.transactional.outbox.producer.mappers;
1+
package me.ioannisioannou.transactional.outbox.employee.mappers;
22

33
import me.ioannisioannou.transactional.outbox.events.EmployeeCreated;
44
import me.ioannisioannou.transactional.outbox.events.EmployeeDeleted;
55
import me.ioannisioannou.transactional.outbox.events.EmployeeUpdated;
6-
import me.ioannisioannou.transactional.outbox.producer.dtos.CreateEmployeeRequest;
7-
import me.ioannisioannou.transactional.outbox.producer.dtos.CreateEmployeeResponse;
8-
import me.ioannisioannou.transactional.outbox.producer.dtos.UpdateEmployeeRequest;
9-
import me.ioannisioannou.transactional.outbox.producer.dtos.UpdateEmployeeResponse;
10-
import me.ioannisioannou.transactional.outbox.producer.entities.Employee;
11-
import me.ioannisioannou.transactional.outbox.producer.events.EnrichedDomainEvent;
6+
import me.ioannisioannou.transactional.outbox.employee.dtos.CreateEmployeeRequest;
7+
import me.ioannisioannou.transactional.outbox.employee.dtos.CreateEmployeeResponse;
8+
import me.ioannisioannou.transactional.outbox.employee.dtos.UpdateEmployeeRequest;
9+
import me.ioannisioannou.transactional.outbox.employee.dtos.UpdateEmployeeResponse;
10+
import me.ioannisioannou.transactional.outbox.employee.entities.Employee;
11+
import me.ioannisioannou.transactional.outbox.employee.events.EnrichedDomainEvent;
1212
import org.springframework.stereotype.Component;
1313

1414
import java.util.UUID;
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
package me.ioannisioannou.transactional.outbox.producer.repositories;
1+
package me.ioannisioannou.transactional.outbox.employee.repositories;
22

3-
import me.ioannisioannou.transactional.outbox.producer.entities.Employee;
3+
import me.ioannisioannou.transactional.outbox.employee.entities.Employee;
44
import org.springframework.data.jpa.repository.JpaRepository;
55
import org.springframework.stereotype.Repository;
66

Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
package me.ioannisioannou.transactional.outbox.producer.repositories;
1+
package me.ioannisioannou.transactional.outbox.employee.repositories;
22

3-
import me.ioannisioannou.transactional.outbox.producer.entities.Outbox;
3+
import me.ioannisioannou.transactional.outbox.employee.entities.Outbox;
44
import org.springframework.data.domain.Page;
55
import org.springframework.data.domain.Pageable;
66
import org.springframework.data.jpa.repository.JpaRepository;
+3-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
package me.ioannisioannou.transactional.outbox.producer.services;
1+
package me.ioannisioannou.transactional.outbox.employee.services;
22

33
import com.amazonaws.services.sns.AmazonSNS;
44
import com.amazonaws.services.sns.model.MessageAttributeValue;
55
import com.amazonaws.services.sns.model.PublishRequest;
66
import lombok.RequiredArgsConstructor;
7-
import me.ioannisioannou.transactional.outbox.producer.entities.Outbox;
8-
import me.ioannisioannou.transactional.outbox.producer.repositories.OutboxRepository;
7+
import me.ioannisioannou.transactional.outbox.employee.entities.Outbox;
8+
import me.ioannisioannou.transactional.outbox.employee.repositories.OutboxRepository;
99
import org.springframework.beans.factory.annotation.Value;
1010
import org.springframework.data.domain.Pageable;
1111
import org.springframework.scheduling.annotation.Scheduled;
+5-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
package me.ioannisioannou.transactional.outbox.producer.services;
1+
package me.ioannisioannou.transactional.outbox.employee.services;
22

33
import lombok.RequiredArgsConstructor;
4-
import me.ioannisioannou.transactional.outbox.producer.entities.Employee;
5-
import me.ioannisioannou.transactional.outbox.producer.exceptions.EmployeeDoesNotExistException;
6-
import me.ioannisioannou.transactional.outbox.producer.mappers.EmployeeMapper;
7-
import me.ioannisioannou.transactional.outbox.producer.repositories.EmployeeRepository;
4+
import me.ioannisioannou.transactional.outbox.employee.entities.Employee;
5+
import me.ioannisioannou.transactional.outbox.employee.exceptions.EmployeeDoesNotExistException;
6+
import me.ioannisioannou.transactional.outbox.employee.mappers.EmployeeMapper;
7+
import me.ioannisioannou.transactional.outbox.employee.repositories.EmployeeRepository;
88
import org.springframework.context.ApplicationEventPublisher;
99
import org.springframework.dao.EmptyResultDataAccessException;
1010
import org.springframework.stereotype.Service;
+4-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
package me.ioannisioannou.transactional.outbox.producer.services;
1+
package me.ioannisioannou.transactional.outbox.employee.services;
22

33
import com.fasterxml.jackson.databind.JsonNode;
44
import com.fasterxml.jackson.databind.ObjectMapper;
55
import lombok.RequiredArgsConstructor;
6-
import me.ioannisioannou.transactional.outbox.producer.entities.Outbox;
7-
import me.ioannisioannou.transactional.outbox.producer.events.EnrichedDomainEvent;
8-
import me.ioannisioannou.transactional.outbox.producer.repositories.OutboxRepository;
6+
import me.ioannisioannou.transactional.outbox.employee.entities.Outbox;
7+
import me.ioannisioannou.transactional.outbox.employee.events.EnrichedDomainEvent;
8+
import me.ioannisioannou.transactional.outbox.employee.repositories.OutboxRepository;
99
import org.springframework.context.event.EventListener;
1010
import org.springframework.stereotype.Service;
1111

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
package me.ioannisioannou.transactional.outbox.producer;
1+
package me.ioannisioannou.transactional.outbox.employee;
22

33
import org.junit.jupiter.api.Test;
44
import org.springframework.boot.test.context.SpringBootTest;
55

66
@SpringBootTest
7-
class ProducerApplicationTests {
7+
class EmployeeApplicationTests {
88

99
@Test
1010
void contextLoads() {

pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
<modules>
1515
<module>domain-events</module>
16-
<module>producer-service</module>
16+
<module>employee-service</module>
1717
<module>consumer-service</module>
1818
</modules>
1919
</project>

producer-service/src/main/java/me/ioannisioannou/transactional/outbox/producer/ProducerApplication.java

-22
This file was deleted.

0 commit comments

Comments
 (0)