Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Event Sourcing with Kotlin

Nico Krijnen
September 20, 2022

Event Sourcing with Kotlin

You've heard about Event Sourcing, it sounds like a useful technique that can potentially make some really hard problems a lot easier. But where do you start? There are many talks about the theory and application of Event Sourcing, but what does it actually look like in code? And why is the Kotlin language such an excellent match for writing event sourced applications?

Presented at: KotlinDevDay 2022

Nico Krijnen

September 20, 2022
Tweet

More Decks by Nico Krijnen

Other Decks in Programming

Transcript

  1. // as function call fun registerUser(email: String, password: String) {

    ... } // using command data class RegisterUser( val email: String, val password: String, ) fun handle(command: RegisterUser) { ... }
  2. Naming ❌ ✅ CreateUser RegisterUser UpdateAddress MoveToAddress UpdateProduct InventoryIncrease DeleteUser

    DeactiveUserRegistration Don't use CRUD Capture intent 
 in language of the domain
  3. data class UserRegisteredEvent( val userId: UUID, val email: String, val

    password: String, ) data class MovedToAddressEvent( val person: UUID, val newAddress: Address, val atTime: Instant, ) data class InventoryIncreasedEvent( val product: UUID, val amount: Int, ) data class UserRegistrationDeactivatedEvent( val userId: UUID, )
  4. data class UserRegisteredEvent( val userId: UUID, val email: String, val

    password: String, ) data class MovedToAddressEvent( val person: UUID, val newAddress: Address, val atTime: Instant, ) data class InventoryIncreasedEvent( val product: UUID, val amount: Int, ) data class UserRegistrationDeactivatedEvent( val userId: UUID, ) data class RegisterUserCommand( val userId: UUID, val email: String, val password: String, ) data class MoveToAddressCommand( val person: UUID, val newAddress: Address, val atTime: Instant, ) data class IncreaseInventoryCommand( val product: UUID, val amount: Int, ) data class DeactivateUserRegistrationCommand( val userId: UUID, ) What is the difference between 
 a command and an event?
  5. Event ≠ Event Event Driven ≠ Event Sourced ≠ Event

    Notification ≠ Integration Events 
 ≠ Event carried state transfer ≠ Domain Events ≠ UI Events
  6. UI RDBMS Elasticsearch Command Query Command model Query model Projection

    Event Write side Read side Query model Projection DynamoDB Query
  7. UI RDBMS Elasticsearch Command Query Command model Query model Projection

    Event Write side Read side Query model Projection DynamoDB Query UI Command model Query model Services Model UI Write services Read services
  8. Traditional Capture events instead of directly updating a consolidated view

    Example: moving to a new house George moved from Apeldoorn to Deventer Traditional
  9. Traditional Model data as events Capture events instead of directly

    updating a consolidated view Example: moving to a new house George moved from Apeldoorn to Deventer Traditional Event sourced odel data as events vents instead of directly updating a consolidated view moving to a new house oved from Apeldoorn to Deventer Traditional Event sourced
  10. When should you use EventSourcing? • When time/history is important...

    maybe • Adjust behavior after the fact... probably • When you need an audit log...
  11. When should you use EventSourcing? • When time/history is important...

    maybe • Adjust behavior after the fact... probably • When you need an audit log... probably not
  12. Lego set booked Rent fee payed Lego set shipped Lego

    set low on stock Lego set returned Photos of Lego build shared
  13. Lego set booked Rent fee payed Lego set shipped Lego

    set low on stock Lego set returned Photos of Lego build shared Book Lego set Payment Provider Ship Lego set Low stock policy Share 
 Lego build photos Return Lego set
  14. Lego set booked Rent fee payed Lego set shipped Lego

    set low on stock Lego set returned Photos of Lego build shared Book Lego set Payment Provider Ship Lego set Low stock policy Share 
 Lego build photos Return Lego set Lego Store Lego Builder Lego Builder Lego Store
  15. Lego set booked Rent fee payed Lego set shipped Lego

    set low on stock Lego set returned Photos of Lego build shared Book Lego set Payment Provider Ship Lego set Low stock policy Share 
 Lego build photos Return Lego set Lego Store Lego Builder Lego Builder Lego Store Lego set inventory Lego set inventory Lego build gallery Lego set inventory
  16. Write model 15/05/2022, 15:16 Page 1 of 1 file:///Users/nico/repos/_workshops/kdd2-event-sourcing/src/media/docs/flow.drawio.svg handleCommand

    API POST /legoset/{nr}/book handleMutation handle(command) ⓸ LegoSetService BookLegoSetCommand existingEvents ⓶ put ⓺ get ⓵ EventStore LegoSetAggregate BookLegoSetCommand newEvents ⓹ constructor(events) ⓷ λ List<LegoSetEvent> List<LegoSetEvent> List<LegoSetEvent> List<LegoSetEvent> KeyValueStore
  17. Write model handleCommand API POST /legoset/{nr}/book handleMutation handle(command) ⓸ LegoSetService

    BookLegoSetCommand existingEvents ⓶ put ⓺ get ⓵ EventStore BookLegoSetCommand newEvents ⓹ λ List<LegoSetEvent> List<LegoSetEvent> List<LegoSetEvent> KeyValueStore
  18. Write model 15/05/2022, 15:16 eCommand handleMutation handle(command) ⓸ LegoSetService oSetCommand

    existingEvents ⓶ EventStore LegoSetAggregate BookLegoSetCommand newEvents ⓹ constructor(events) ⓷ λ List<LegoSetEvent> List<LegoSetEvent> List<LegoSetEvent> List<LegoSetEvent>
  19. Write model handleCommand API POST /legoset/{nr}/book handleMutation handle(command) ⓸ LegoSetService

    BookLegoSetCommand existingEvents ⓶ put ⓺ get ⓵ EventStore BookLegoSetCommand newEvents ⓹ λ List<LegoSetEvent> List<LegoSetEvent> List<LegoSetEvent> KeyValueStore
  20. data class LegoSetBookedEvent( val userId: String, val userName: String, val

    cardTypeId: Int, val cardNumber: String, val cardSecurityNumber: String, val cardHolderName: String, val cardExpiration: Instant, val order: Order, )
  21. data class LegoSetBookedEvent( val userId: String, val userName: String, val

    cardTypeId: Int, val cardNumber: String, val cardSecurityNumber: String, val cardHolderName: String, val cardExpiration: Instant, val order: Order, ) public record LegoSetBookedEvent( @NotNull String userId, @NotNull String userName, int cardTypeId, @NotNull String cardNumber, @NotNull String cardSecurityNumber, @NotNull String cardHolderName, @NotNull Instant cardExpiration, @NotNull Order order ) { public LegoSetBookedEvent { Objects.requireNonNull(userId, "userId"); Objects.requireNonNull(userName, "userName"); Objects.requireNonNull(cardNumber, "cardNumber"); Objects.requireNonNull(cardSecurityNumber, "cardSecurityNumber"); Objects.requireNonNull(cardHolderName, "cardHolderName"); Objects.requireNonNull(cardExpiration, "cardExpiration"); Objects.requireNonNull(order, "order"); } }
  22. public class LegoSetBookedEvent { @NotNull private final String userId; @NotNull

    private final String userName; private final int cardTypeId; @NotNull private final String cardNumber; @NotNull private final String cardSecurityNumber; @NotNull private final String cardHolderName; @NotNull private final Instant cardExpiration; @NotNull private final Order order; public LegoSetBookedEvent( @NotNull String userId, @NotNull String userName, int cardTypeId, @NotNull String cardNumber, @NotNull String cardSecurityNumber, @NotNull String cardHolderName, @NotNull Instant cardExpiration, @NotNull Order order ) { Objects.requireNonNull(userId, "userId"); Objects.requireNonNull(userName, "userName"); Objects.requireNonNull(cardNumber, "cardNumber"); Objects.requireNonNull(cardSecurityNumber, "cardSecurityNumber"); Objects.requireNonNull(cardHolderName, "cardHolderName"); Objects.requireNonNull(cardExpiration, "cardExpiration"); Objects.requireNonNull(order, "order"); this.userId = userId; this.userName = userName; this.cardTypeId = cardTypeId; this.cardNumber = cardNumber; this.cardSecurityNumber = cardSecurityNumber; this.cardHolderName = cardHolderName; this.cardExpiration = cardExpiration; this.order = order; } @NotNull public final String getUserId() { return this.userId; } @NotNull public final String getUserName() { return this.userName; } public final int getCardTypeId() { return this.cardTypeId; } @NotNull public final String getCardNumber() { return this.cardNumber; } @NotNull public final String getCardSecurityNumber() { return this.cardSecurityNumber; } @NotNull public final String getCardHolderName() { return this.cardHolderName; } @NotNull public final Instant getCardExpiration() { return this.cardExpiration; } @NotNull public final Order getOrder() { return this.order; } @NotNull public String toString() { ... } public int hashCode() { ... } public boolean equals(@Nullable Object var1) { ... } } data class LegoSetBookedEvent( val userId: String, val userName: String, val cardTypeId: Int, val cardNumber: String, val cardSecurityNumber: String, val cardHolderName: String, val cardExpiration: Instant, val order: Order, ) public record LegoSetBookedEvent( @NotNull String userId, @NotNull String userName, int cardTypeId, @NotNull String cardNumber, @NotNull String cardSecurityNumber, @NotNull String cardHolderName, @NotNull Instant cardExpiration, @NotNull Order order ) { public LegoSetBookedEvent { Objects.requireNonNull(userId, "userId"); Objects.requireNonNull(userName, "userName"); Objects.requireNonNull(cardNumber, "cardNumber"); Objects.requireNonNull(cardSecurityNumber, "cardSecurityNumber"); Objects.requireNonNull(cardHolderName, "cardHolderName"); Objects.requireNonNull(cardExpiration, "cardExpiration"); Objects.requireNonNull(order, "order"); } } ≦ 13
  23. data class LegoSetBookedEvent( val userId: String, val userName: String, val

    cardTypeId: Int, val cardNumber: String, val cardSecurityNumber: String, val cardHolderName: String, val cardExpiration: Instant, val order: Order, )
  24. data class LegoSetAddedToCatalogEvent( override val legoSet: LegoSetNumber, override val atTime:

    Instant = Instant.now(), ) : LegoSetEvent() data class LegoSetBookedEvent( override val legoSet: LegoSetNumber, val builder: BuilderId, override val atTime: Instant = Instant.now(), ) : LegoSetEvent() data class LegoSetReturnedEvent( override val legoSet: LegoSetNumber, val builder: BuilderId, override val atTime: Instant = Instant.now(), ) : LegoSetEvent() data class LegoSetInventoryDecreasedEvent( override val legoSet: LegoSetNumber, val amount: Int, override val atTime: Instant = Instant.now(), ) : LegoSetEvent() data class LegoSetInventoryIncreasedEvent( override val legoSet: LegoSetNumber, val amount: Int, override val atTime: Instant = Instant.now(), ) : LegoSetEvent() Events.kt
  25. sealed interface LegoSetCommand { val legoSet: LegoSetNumber } data class

    AddLegoSetToCatalogCommand( override val legoSet: LegoSetNumber ) : LegoSetCommand data class BookLegoSetCommand( override val legoSet: LegoSetNumber, val builder: BuilderId, ) : LegoSetCommand data class ReturnLegoSetCommand( override val legoSet: LegoSetNumber, val builder: BuilderId, ) : LegoSetCommand
  26. sealed interface LegoSetCommand { val legoSet: LegoSetNumber } fun handleCommand(command:

    LegoSetCommand) = when (command) { is AddLegoSetToCatalogCommand -> add(command) is BookLegoSetCommand -> bookSet(command) is ReturnLegoSetCommand -> returnSet(command) } private fun add(command: AddLegoSetToCatalogCommand) = ... private fun bookSet(command: BookLegoSetCommand) = ... private fun returnSet(command: ReturnLegoSetCommand) = ...
  27. private fun List<LegoSetEvent>.amountInStock(): Int = this.fold(0) { result, event ->

    when (event) { is InventoryAffectingEvent -> result + event.amountChanged else -> result } }
  28. private val available: Boolean by lazy { amountInStock > 0

    } private val amountInStock: Int by lazy { events.amountInStock() } private fun List<LegoSetEvent>.amountInStock(): Int = this.fold(0) { result, event -> when (event) { is InventoryAffectingEvent -> result + event.amountChanged else -> result } }
  29. private fun List<LegoSetEvent>.amountInStock(): Int = this.fold(0) { result, event ->

    when (event) { is InventoryAffectingEvent -> result + event.amountChanged else -> result } } private val available: Boolean by lazy { amountInStock > 0 } private val amountInStock: Int by lazy { events.amountInStock() } private fun bookSet(command: BookLegoSetCommand): ... { if (!available) throw LegoSetNotAvailableException(command.legoSet) return listOf(LegoSetBookedEvent(command.legoSet, command.builder)) }
  30. That looked simple enough! • Define commands and events •

    Pipeline for handling write model mutations • Aggregates to contain domain logic • Projections to build read models
  31. But did you think of... • Dealing with replays •

    Dealing with eventual consistency • Consistency of write model and event publication
  32. That sounds like I need a framework! • Let it

    solve all the hard stuff, you focus on your domain logic • Flexibility to choose or switch underlying technologies
  33. It's a tradeoff • Frameworks have an opinion, do you

    agree? • Frameworks can be intrusive • Frameworks introduce abstractions you may not need
  34. Recap • Do we like Event Sourcing with Kotlin? YES!

    • Should you use Event Sourcing?
  35. Recap • Do we like Event Sourcing with Kotlin? YES!

    • Should you use Event Sourcing? MAYBE?
  36. Recap • Do we like Event Sourcing with Kotlin? YES!

    • Should you use Event Sourcing? MAYBE? • Framework? Or is plain Kotlin code enough?
  37. Recap • Do we like Event Sourcing with Kotlin? YES!

    • Should you use Event Sourcing? MAYBE? • Framework? Or is plain Kotlin code enough? PROBABLY NOT! (and YES for your domain)