a (very!) simplistic service takes registration orders for new users.
- Successfull registration requests will return a 201 (Created), that carries the URI, under which the data of the newly registered user can be accessed in the `Location`-header:
+ ```shell
+ $ echo peter | http :8080/users
-`echo peter | http :8080/users
HTTP/1.1 201
Content-Length: 0
Date: Fri, 05 Feb 2021 14:44:51 GMT
Location: http://localhost:8080/users/peter
- `
+ ```
- Requests to registrate an already existing user will result in a 400 (Bad Request):
+ ```
+ $ echo peter | http :8080/users
-`echo peter | http :8080/users
HTTP/1.1 400
Connection: close
Content-Length: 0
Date: Fri, 05 Feb 2021 14:44:53 GMT
- `
+ ```
- Successfully registrated users can be listed:
- `http :8080/users
+ ```
+ $ http :8080/users
+
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Fri, 05 Feb 2021 14:53:59 GMT
},
...
]
- `
+ ```
## The Messaging Use-Case
The users are stored in a database and the creation of a new user happens in a transaction.
A "brilliant" colleague came up with the idea, to trigger an `IncorrectResultSizeDataAccessException` to detect duplicate usernames:
-
-`User user = new User(username);
+```java
+User user = new User(username);
repository.save(user);
// Triggers an Exception, if more than one entry is found
repository.findByUsername(username);
-`
+```
The query for the user by its names triggers an `IncorrectResultSizeDataAccessException`, if more than one entry is found.
The uncaught exception will mark the transaction for rollback, hence, canceling the requested registration.
The 400-response is then generated by a corresponding `ExceptionHandler`:
-
-`@ExceptionHandler
+```java
+@ExceptionHandler
public ResponseEntity incorrectResultSizeDataAccessException(
IncorrectResultSizeDataAccessException e)
{
LOG.info("User already exists!");
return ResponseEntity.badRequest().build();
}
-`
+```
Please do not code this at home...
In the example implementation I am using an `EventPublisher` to decouple the business logic from the implementation of the messaging.
The controller publishes an event, when a new user is registered:
-`publisher.publishEvent(new UserEvent(this, usernam));
-`
+```java
+publisher.publishEvent(new UserEvent(this, usernam));
+```
A listener annotated with `@TransactionalEventListener` receives the events and handles the messaging:
-
-`@TransactionalEventListener
+```java
+@TransactionalEventListener
public void onUserEvent(UserEvent event)
{
// Sending the message happens here...
}
-`
+```
In non-critical use-cases, it might be sufficient to actually send the message to Kafka right here.
Spring ensures, that the message of the listener is only called, if the transaction completes successfully.
The generated **unique and monotonically increasing id** is required later, for the implementation of **Exactly-Once** semantics.
[The SQL for the table](https://github.com/juplo/demos-spring-data-jdbc/blob/part-1/src/main/resources/db/migration/h2/V2__Table_outbox.sql) looks like this:
-
- `CREATE TABLE outbox (
+```sql
+CREATE TABLE outbox (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
key VARCHAR(127),
value varchar(1023),
issued timestamp
);
-`
+```
## Decoupling The Business Logic
In order to decouple the business logic from the implementation of the messaging mechanism, I have implemented a thin layer, that uses [Spring Application Events](https://docs.spring.io/spring-integration/docs/current/reference/html/event.html) to publish the messages.
Messages are send as a [subclass of `ApplicationEvent`](https://github.com/juplo/demos-spring-data-jdbc/blob/part-1/src/main/java/de/juplo/kafka/outbox/OutboxEvent.java):
-
-`publisher.publishEvent(
+```java
+publisher.publishEvent(
new UserEvent(
this,
username,
CREATED,
ZonedDateTime.now(clock)));
-`
+```
The event takes a key ( `username`) and an object as value (an instance of an enum in our case).
An `EventListener` receives the events and writes them in the outbox table:
-
-`@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
+```
+@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void onUserEvent(OutboxEvent event)
{
try
throw new RuntimeException(e);
}
}
-`
+```
The `@TransactionalEventListener` is not really needed here.
A normal `EventListener` would also suffice, because spring immediately executes all registered normal event listeners.