Skip to content

Commit

Permalink
Day 26 task
Browse files Browse the repository at this point in the history
  • Loading branch information
pawelpluta committed Jan 27, 2023
1 parent b7da557 commit 256bc71
Show file tree
Hide file tree
Showing 21 changed files with 452 additions and 1 deletion.
3 changes: 2 additions & 1 deletion README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ Every task will focus on Java, popular libraries and frameworks. So, let's start
* [Day 22](/day022/README.MD)
* [Day 23](/day023/README.MD)
* [Day 24](/day024/README.MD)
* [Day 25](/day025/README.MD)
* [Day 25](/day025/README.MD)
* [Day 26](/day026/README.MD)
8 changes: 8 additions & 0 deletions day026/README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Day 26 - transactional outbox

Today's task is to implement a pattern that sends out events and persist aggregate changes in single transaction.
Usually it is implemented on database with a table dedicated for events, where they are stored.
Events are picked up from this table and sent by another thread.

The most important for this pattern to work is to persist changes and events in the same transaction.
For task clearness purposes, tests for transaction checks are skipped, but feel free to write them.
33 changes: 33 additions & 0 deletions day026/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
plugins {
id 'org.springframework.boot' version '3.0.1'
id 'io.spring.dependency-management' version '1.1.0'
id 'java'
}

group 'com.pawelpluta'
version '0.0.1'

repositories {
mavenCentral()
}

dependencies {
runtimeOnly 'org.postgresql:postgresql'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.testcontainers:testcontainers:1.17.6'
testImplementation 'org.testcontainers:postgresql:1.17.6'
testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.13.3'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.1'
}

test {
useJUnitPlatform()
}

java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(19))
}
}
1 change: 1 addition & 0 deletions day026/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootProject.name = 'day024'
99 changes: 99 additions & 0 deletions day026/src/main/java/com/pawelpluta/day026/BankAccount.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.pawelpluta.day026;

import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

public class BankAccount {

private final String accountNumber;
private final BigDecimal balance;
private final List<Event> events;

private BankAccount(String accountNumber, BigDecimal balance, List<Event> events) {
this.accountNumber = accountNumber;
this.balance = balance;
this.events = events;
}

public String accountNumber() {
return accountNumber;
}

public BigDecimal balance() {
return balance;
}

public List<Event> events() {
return events;
}

public static BankAccount of(String accountNumber, BigDecimal balance) {
return new BankAccount(accountNumber, balance, Collections.emptyList());
}
BankAccount handle(ChargeAccountCommand command) {
if (balance.compareTo(command.amount()) >= 0) {
BigDecimal updatedBalance = balance.subtract(command.amount());
return new BankAccount(
accountNumber,
updatedBalance,
List.of(new BankAccountBalanceDecreased(accountNumber, balance, updatedBalance)));
} else {
return new BankAccount(
accountNumber,
balance,
List.of(new AccountChargeRejectedDueToInsufficientFounds(accountNumber, balance, command.amount())));
}
}

static class BankAccountBalanceDecreased implements Event {

private final String id;
private final String accountNumber;
private final BigDecimal oldBalance;
private final BigDecimal newBalance;

BankAccountBalanceDecreased(String accountNumber, BigDecimal oldBalance, BigDecimal newBalance) {
id = UUID.randomUUID().toString();
this.accountNumber = accountNumber;
this.oldBalance = oldBalance;
this.newBalance = newBalance;
}

@Override
public String id() {
return id;
}

@Override
public String jsonBody() {
return "{}";
}
}

static class AccountChargeRejectedDueToInsufficientFounds implements Event {

private final String id;
private final String accountNumber;
private final BigDecimal balance;
private final BigDecimal charge;

AccountChargeRejectedDueToInsufficientFounds(String accountNumber, BigDecimal balance, BigDecimal charge) {
id = UUID.randomUUID().toString();
this.accountNumber = accountNumber;
this.balance = balance;
this.charge = charge;
}

@Override
public String id() {
return id;
}

@Override
public String jsonBody() {
return "{}";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.pawelpluta.day026;

import java.util.Optional;

public interface BankAccountRepository {
Optional<BankAccount> findById(String id);
void save(BankAccount account);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.pawelpluta.day026;

import java.math.BigDecimal;

record ChargeAccountCommand(String accountId, BigDecimal amount) {
}
18 changes: 18 additions & 0 deletions day026/src/main/java/com/pawelpluta/day026/CommandHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.pawelpluta.day026;

import org.springframework.stereotype.Service;

@Service
class CommandHandler {

private final BankAccountRepository accountRepository;

CommandHandler(BankAccountRepository accountRepository) {
this.accountRepository = accountRepository;
}

void handle(ChargeAccountCommand command) {
accountRepository.findById(command.accountId())
.ifPresent(bankAccount -> accountRepository.save(bankAccount.handle(command)));
}
}
8 changes: 8 additions & 0 deletions day026/src/main/java/com/pawelpluta/day026/Event.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.pawelpluta.day026;

public interface Event {
String id();
String jsonBody();

record EventVO(String id, String jsonBody) implements Event {}
}
7 changes: 7 additions & 0 deletions day026/src/main/java/com/pawelpluta/day026/EventSupplier.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.pawelpluta.day026;

import java.util.List;

public interface EventSupplier {
List<Event> fetchEventsToSend();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.pawelpluta.day026;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
class TransactionalOutboxApp {

public static void main(String[] args) {
SpringApplication.run(TransactionalOutboxApp.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.pawelpluta.day026.jpa;

import com.pawelpluta.day026.BankAccount;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import java.math.BigDecimal;

@Entity
@Table(name = "accounts")
class BankAccountEntity {
@Id
@Column(name = "account_id")
private String id;
@Column(name = "balance")
private BigDecimal balance;

public BankAccountEntity() {
}

public BankAccountEntity(String id, BigDecimal balance) {
this.id = id;
this.balance = balance;
}

static BankAccountEntity from(BankAccount account) {
return new BankAccountEntity(
account.accountNumber(),
account.balance());
}

BankAccount toModel() {
return BankAccount.of(id, balance);
}
}
42 changes: 42 additions & 0 deletions day026/src/main/java/com/pawelpluta/day026/jpa/EventEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.pawelpluta.day026.jpa;

import com.pawelpluta.day026.Event;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "events_outbox")
class EventEntity {
@Id
@Column(name = "event_id")
private String id;
@Column(name = "body")
private String body;
@Column(name = "sent")
private Boolean sent;

public EventEntity() {
}

public EventEntity(String id, String body) {
this.id = id;
this.body = body;
sent = false;
}

static EventEntity from(Event event) {
return new EventEntity(
event.id(),
event.jsonBody());
}

String getId() {
return id;
}

String getBody() {
return body;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.pawelpluta.day026.jpa;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
interface JpaBankAccountEntityRepository extends JpaRepository<BankAccountEntity, String> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.pawelpluta.day026.jpa;

import com.pawelpluta.day026.BankAccount;
import com.pawelpluta.day026.BankAccountRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
class JpaBankAccountRepository implements BankAccountRepository {

private final JpaBankAccountEntityRepository repository;

JpaBankAccountRepository(JpaBankAccountEntityRepository repository) {
this.repository = repository;
}

@Override
public Optional<BankAccount> findById(String id) {
return repository.findById(id).map(BankAccountEntity::toModel);
}

@Override
public void save(BankAccount account) {
repository.save(BankAccountEntity.from(account));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.pawelpluta.day026.jpa;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
interface JpaEventEntityRepository extends JpaRepository<EventEntity, String> {

List<EventEntity> findAllBySentIsFalse();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.pawelpluta.day026.jpa;

import com.pawelpluta.day026.Event;
import com.pawelpluta.day026.EventSupplier;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
class JpaEventPublisherRepository implements EventSupplier {

private final JpaEventEntityRepository repository;

JpaEventPublisherRepository(JpaEventEntityRepository repository) {
this.repository = repository;
}

@Override
public List<Event> fetchEventsToSend() {
return repository.findAllBySentIsFalse().stream().map(entity -> (Event) new Event.EventVO(entity.getId(), entity.getBody())).toList();
}
}
Loading

0 comments on commit 256bc71

Please sign in to comment.