Skip to content

stepin/kotlin-event-sourcing-app

Repository files navigation

kotlin-event-sourcing-app

Π­Ρ‚ΠΎ стартовый шаблон для Π½ΠΎΠ²Ρ‹Ρ… event-sourcing ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΉ Π½Π° ΠšΠΎΡ‚Π»ΠΈΠ½.

Π‘Ρ‚Π°Ρ€Ρ‚ΠΎΠ²Ρ‹ΠΉ шаблон прилоТСния Event sourcing: https://github.com/stepin/kotlin-event-sourcing-app

Π’ Π·Π°ΠΌΠ΅Ρ‚ΠΊΠ΅ "ΠšΠ»Π°ΡΡΠΈΡ‡Π΅ΡΠΊΠΈΠΉ event sourcing" Ρ€Π°Π·ΠΎΠ±Ρ€Π°Π½Ρ‹ основы, Π² "Inline event sourcing" Ρ€Π°Π·ΠΎΠ±Ρ€Π°Π½Π° Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π° этого шаблона.

Π”Π°Π½Π½Ρ‹ΠΉ Ρ€Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ прСдставляСт ΠΈΠ· сСбя ΠΏΡ€ΠΈΠΌΠ΅Ρ€ прилоТСния, Π° Π½Π΅ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΉ Π΄Π²ΠΈΠΆΠΎΠΊ. Пока Ρ‡Ρ‚ΠΎ Ρƒ мСня Π½Π΅Ρ‚ увСрСнности, Ρ‡Ρ‚ΠΎ этот Π΄Π²ΠΈΠΆΠΎΠΊ ΠΌΠΎΠΆΠ½ΠΎ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ "ΠΊΠ°ΠΊ Π΅ΡΡ‚ΡŒ" Π² Π΄Ρ€ΡƒΠ³ΠΈΡ… прилоТСниях. ΠŸΡ€ΠΈ этом Π΅ΡΡ‚ΡŒ ΡƒΠ²Π΅Ρ€Π΅Π½Π½ΠΎΡΡ‚ΡŒ, Ρ‡Ρ‚ΠΎ Π½Π°Ρ‡Π°Π² с этого шаблона, Π²ΠΏΠΎΠ»Π½Π΅ Ρ€Π΅Π°Π»ΡŒΠ½ΠΎ Ρ€Π°Π·Π²ΠΈΠ²Π°Ρ‚ΡŒ прилоТСния.

Π”Π°Π½Π½Ρ‹ΠΉ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ являСтся ΠΈΠ·Π²Π»Π΅Ρ‡Π΅Π½ΠΈΠ΅ΠΌ ΠΎΠ±Ρ‰Π΅ΠΉ части ΠΈΠ· ΠΎΠ΄Π½ΠΎΠ³ΠΎ ΠΈΠ· ΠΌΠΎΠΈΡ… Π»ΠΈΡ‡Π½Ρ‹Ρ… ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ΠΎΠ². Π­Ρ‚ΠΎ ΡƒΠΆΠ΅ Π³Π΄Π΅-Ρ‚ΠΎ 5ая вСрсия Π΄Π²ΠΈΠΆΠΊΠ° (пСрвая Π²ΠΎΠΎΠ±Ρ‰Π΅ Π±Ρ‹Π»Π° Π½Π° Golang). ΠŸΡ€ΠΈ этом вСрсия новая -- Π²ΠΎΠ·ΠΌΠΎΠΆΠ½Ρ‹ ΠΊΠ°ΠΊΠΈΠ΅-Ρ‚ΠΎ ΡˆΠ΅Ρ€ΠΎΡ…ΠΎΠ²Π°Ρ‚ΠΎΡΡ‚ΠΈ ΠΏΠ΅Ρ€Π²ΠΎΠ΅ врСмя.

Π¨Π°Π±Π»ΠΎΠ½ основываСтся Π½Π° ΠΌΠΎΠ΅ΠΌ Π±Π°Π·ΠΎΠ²ΠΎΠΌ шаблонС ΠšΠΎΡ‚Π»ΠΈΠ½-ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΉ: https://github.com/stepin/kotlin-bootstrap-app

Бобытия

НачинаСм с выявлСния событий ΠΈ сущностСй.

Допустим, Ρƒ нас Π΅ΡΡ‚ΡŒ простая бизнСс-ΡΡƒΡ‰Π½ΠΎΡΡ‚ΡŒ ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ:

data class User(
  val displayName: String,
  val firstName: String,
  val seconfName: String,
  val email: String
)

И ΠΌΡ‹ Ρ…ΠΎΡ‚ΠΈΠΌ ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΠΈΠ΅ сцСнарии (события):

  • рСгистрация ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ
  • смСна ΠΈΠΌΠ΅Π½ΠΈ
  • ΡƒΠ΄Π°Π»Π΅Π½ΠΈΠ΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ

Для простоты ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π° Π½Π΅ Π±ΡƒΠ΄Π΅ΠΌ ΠΎΠ±Ρ€Π°Ρ‰Π°Ρ‚ΡŒ Π²Π½ΠΈΠΌΠ°Π½ΠΈΠ΅ Π½Π° подтвСрТдСния ΠΈ Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΡŽ.

ΠŸΡ€ΠΈΠΌΠ΅Ρ€ события рСгистрации ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ:

data class UserRegistered(
  val email: String,
  val firstName: String?,
  val secondName: String?,
  val displayName: String,
  override val accountGuid: AccountGuid,
  override val aggregatorGuid: UserGuid = UUID.randomUUID(),
  override val guid: EventGuid = UUID.randomUUID(),
) : UserEvent(eventTypeVersion = 3)
  • 4 основных поля: email, firstName, secondName, displayName
  • guid самого событий (Ρ€Π°Π½Π΄ΠΎΠΌΠ½Ρ‹ΠΉ)
  • aggregator guid = user guid -- Π²ΠΎΡ‚ это Π½Π΅ΡƒΠ΄ΠΎΠ±Π½ΠΎ, Ρ‡Ρ‚ΠΎ Π½Π΅Ρ‚ синонима, Π½ΠΎ ΠΌΠΎΠΆΠ½ΠΎ ΠΏΡ€ΠΈΠ²Ρ‹ΠΊΠ½ΡƒΡ‚ΡŒ (ΠΈ ΡƒΠΊΠ°Π·Π°Π½ typealias UserGuid)
  • account guid -- Π΄Π²ΠΈΠΆΠΎΠΊ расчитан Π½Π° ΠΌΡƒΠ»ΡŒΡ‚ΠΈΠ°ΠΊΠΊΠ°Π½Ρ‚ΠΎΠ²Ρ‹Π΅ прилоТСния
  • data class -- ΡƒΠ΄ΠΎΠ±Π½ΠΎ. И Π΅Ρ‰Π΅ ΡƒΠ΄ΠΎΠ±Π½Π΅Π΅, Ρ‡Ρ‚ΠΎ UserEvent -- sealed класс, ΠΌΠΎΠΆΠ½ΠΎ Ρ‚Π°ΠΊΠΈΠ΅ конструкции Π΄Π΅Π»Π°Ρ‚ΡŒ:
when (val e = event as UserEvent) {
  is UserMetaUpdated -> "updated $e"
  is UserRegistered -> "user registered with id $id ${meta.createdAt} $e"
  is UserRemoved -> "user ${e.email} deleted at ${meta.createdAt}"
}

Π‘Π°Π·ΠΎΠ²Ρ‹ΠΉ класс для событий Π°Π³Ρ€Π΅Π³Π°Ρ‚Π° User выглядит Ρ‚Π°ΠΊ:

sealed class UserEvent(
  override val eventTypeVersion: Short = 0,
) : DomainEvent {
  override val aggregatorType: String
    get() = "user"

  override val eventType: String
    get() = this.javaClass.simpleName

  abstract override val aggregatorGuid: UserGuid
}
  • рСализуСтся интСрфСйс DomainEvent Π΄Π²ΠΈΠΆΠΊΠ°
  • выставляСтся typealias UserGuid для aggregatorGuid -- Π½Π΅ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎ, ΠΊΠ°ΠΊ докумСнтация
  • выставляСтся Ρ‚ΠΈΠΏ Π°Π³Ρ€Π΅Π³Π°Ρ‚Π°
  • выставляСтся Ρ‚ΠΈΠΏ события -- автоматичСски бСрСтся имя класса события (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, UserRegistered)
  • выставляСтся вСрсия события Π² 0 ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ, Π½ΠΎ это Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ событиС ΠΌΠΎΠΆΠ΅Ρ‚ ΠΏΠ΅Ρ€Π΅ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΠΈΡ‚ΡŒ

По сути, ΠΎΡ‚ события Π΄Π²ΠΈΠΆΠΎΠΊ Ρ‚Ρ€Π΅Π±ΡƒΠ΅Ρ‚ 2 Π²Π΅Ρ‰ΠΈ:

  • Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ интСрфСйса DomainEvent
  • ΠΊΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½ΠΎΠΉ сСриализации ΠΈ дСсСриализации JSONB

ΠžΡΡ‚Π°Π»ΡŒΠ½ΠΎΠ΅ Π½Π° усмотрСниС Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠ°. ΠŸΡ€ΠΈ этом Π±Π°Π·ΠΎΠ²Ρ‹ΠΉ класс для всСх событий Π°Π³Ρ€Π΅Π³Π°Ρ‚Π° считаСтся Ρ…ΠΎΡ€ΠΎΡˆΠ΅ΠΉ ΠΏΡ€Π°ΠΊΡ‚ΠΈΠΊΠΎΠΉ.

ΠŸΡ€ΠΎ id/guid: Π² этом ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π΅ подразумСваСтся, Ρ‡Ρ‚ΠΎ ΠΊΠΎΠΌΠ°Π½Π΄Ρ‹ Ρ€Π°Π±ΠΎΡ‚Π°ΡŽΡ‚ с guid, Π° ΠΏΡ€ΠΈ нСобходимости join Π² SQL-запросах ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ id (Ρ‚.ΠΊ. быстрСС).

Команда

Π£ нас ΠΊΠΎΠΌΠ°Π½Π΄Π° -- это Π»ΠΈΠ±ΠΎ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΉ Spring-сСрвис, Π»ΠΈΠ±ΠΎ ΠΌΠ΅Ρ‚ΠΎΠ΄ Π²Π½ΡƒΡ‚Ρ€ΠΈ Spring-сСрвиса. По сути СдинствСнный ΠΊΡ€ΠΈΡ‚ΠΈΡ‡Π½Ρ‹ΠΉ ΠΌΠΎΠΌΠ΅Π½Ρ‚ -- Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒΡΡ интСрфСйс EventStorePublish для ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ событий, Π° ΠΎΡΡ‚Π°Π»ΡŒΠ½ΠΎΠ΅ Π΄Π²ΠΈΠΆΠΎΠΊ Π½Π΅ ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡ΠΈΠ²Π°Π΅Ρ‚.

Команда рСгистрации:

@Service
class RegisterUser(
  private val store: EventStorePublisher,
  private val userRepository: UserRepository,
) {

data class Params(
  val email: String,
  val firstName: String?,
  val secondName: String?,
  val displayName: String?,
)

sealed class Response {
  data class Created(val userGuid: UUID) : Response()
  data class Error(val errorCode: ErrorCode) : Response()
}

suspend fun execute(params: Params): Response = with(params) {
  val user = userRepository.findByEmail(email)
  if (user != null) {
    return Response.Error(ErrorCode.USER_ALREADY_REGISTERED)
  }

  val accountGuid = UUID.randomUUID()
  val userGuid = UUID.randomUUID()

  val userRegistered = UserRegistered(
    accountGuid = accountGuid,
    aggregatorGuid = userGuid,
    email = email,
    firstName = firstName,
    secondName = secondName,
    displayName = displayName ?: calcDisplayName(email, firstName, secondName),
  )
  store.publish(userRegistered)

  val accountCreated = AccountCreated(
    name = "НСизвСстная компания",
    accountGuid = accountGuid,
    userGuid = userGuid,
  )
  store.publish(accountCreated)

  return Response.Created(userGuid)
}
}

Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌΡ‹Π΅ ΠΎΡ‚ ΠΊΠΎΠΌΠ°Π½Π΄ значСния зависят ΠΎΡ‚ бизнСс-Π»ΠΎΠ³ΠΈΠΊΠΈ: ΠΌΠΎΠ³ΡƒΡ‚ Π»ΠΈ Π±Ρ‹Ρ‚ΡŒ бизнСс-ошибки, Π½ΡƒΠΆΠ½ΠΎ Π»ΠΈ Π²Π΅Ρ€Π½ΡƒΡ‚ΡŒ guid ΠΈ Ρ‚.ΠΏ. Π’ ΠΊΠ°ΠΊΠΈΡ…-Ρ‚ΠΎ случаях ΠΌΠΎΠΆΠ΅Ρ‚ Π½ΠΈΡ‡Π΅Π³ΠΎ Π½Π΅ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Ρ‚ΡŒΡΡ.

ΠŸΡ€ΠΎΠ΅ΠΊΡ‚ΠΎΡ€Ρ‹

ΠŸΡ€ΠΈΠΌΠ΅Ρ€ 2-Ρ… ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ΠΎΡ€ΠΎΠ² Π² ΠΎΠ΄Π½ΠΎΠΌ классС:

@Service
class UserProjector(
  private val userRepository: UserRepository,
  private val accountRepository: AccountRepository,
) {
  companion object : Logging

  @Projector
  suspend fun handleUserRegistered(e: UserRegistered, meta: EventMetadata) {
    val account = accountRepository.findByGuid(e.accountGuid)

    val u = UserEntity()
    u.accountGuid = e.accountGuid
    u.accountId = account?.id ?: 0
    u.guid = e.aggregatorGuid
    u.email = e.email
    u.displayName = e.displayName
    u.firstName = e.firstName
    u.secondName = e.secondName
    u.createdAt = meta.createdAt.toInstant(ZoneOffset.UTC)

    val savedUser = userRepository.save(u)
    logger.debug { "new user id: ${savedUser.id}" }
  }

  @Projector
  suspend fun handleUserRemoved(e: UserRemoved) {
    val user = getUser(e.aggregatorGuid)
    userRepository.delete(user)
  }

  private suspend fun getUser(userGuid: UUID) = userRepository.findByGuid(userGuid)
    ?: throw DomainException(ErrorCode.USER_NOT_FOUND)
}
  • ΠΌΠ΅Ρ‚ΠΎΠ΄ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ΠΎΡ€Π° Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ Π² Spring-Π±ΠΈΠ½Π΅
  • Π΄ΠΎΠ»ΠΆΠ½Π° Π±Ρ‹Ρ‚ΡŒ аннотация @Projector
  • Π² классС ΠΌΠΎΠΆΠ΅Ρ‚ Π±Ρ‹Ρ‚ΡŒ нСсколько ΠΌΠ΅Ρ‚ΠΎΠ΄ΠΎΠ² -- ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΈΠΉ Π½Π΅Ρ‚
  • ΠΏΠ΅Ρ€Π²Ρ‹ΠΉ Π°Ρ€Π³ΡƒΠΌΠ΅Π½Ρ‚ -- событиС
  • Π²Ρ‚ΠΎΡ€ΠΎΠΉ (ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ) -- ΠΌΠ΅Ρ‚Π°Π΄Π°Π½Π½Ρ‹Π΅ события
  • ΠΌΠ΅Ρ‚ΠΎΠ΄ Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ suspend (Π² ΠΏΡ€ΠΈΠ½Ρ†ΠΈΠΏΠ΅, это ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΈΠ΅ ΠΌΠΎΠΆΠ½ΠΎ ΡΠ½ΡΡ‚ΡŒ, Π½ΠΎ сСйчас Ρ‚Π°ΠΊ Π² Π΄Π²ΠΈΠΆΠΊΠ΅ ΠΈ Π½Π΅ ΠΏΠ»Π°Π½ΠΈΡ€ΡƒΡŽ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π½Π΅ suspend-ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹)
  • ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ Π² ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ΠΎΡ€Π΅ ΠΎΡ‚ΠΌΠ΅Π½ΠΈΡ‚ сохранСниС события

Π Π΅Π°ΠΊΡ‚ΠΎΡ€Ρ‹

@Service
class UserRegisteredEmailReactor(
  private val emailService: SendEmailService,
) {
  companion object : Logging

  @Reactor
  suspend fun handle(e: UserRegistered) {
    emailService.sendEmailConfirmationEmail(e.displayName, e.email, e.aggregatorGuid.toString())
  }
}
  • ΠΌΠ΅Ρ‚ΠΎΠ΄ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ΠΎΡ€Π° Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ Π² Spring-Π±ΠΈΠ½Π΅
  • Π΄ΠΎΠ»ΠΆΠ½Π° Π±Ρ‹Ρ‚ΡŒ аннотация @Reactor
  • Π² классС ΠΌΠΎΠΆΠ΅Ρ‚ Π±Ρ‹Ρ‚ΡŒ нСсколько ΠΌΠ΅Ρ‚ΠΎΠ΄ΠΎΠ² -- ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΈΠΉ Π½Π΅Ρ‚
  • ΠΏΠ΅Ρ€Π²Ρ‹ΠΉ Π°Ρ€Π³ΡƒΠΌΠ΅Π½Ρ‚ -- событиС
  • Π²Ρ‚ΠΎΡ€ΠΎΠΉ (ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ) -- ΠΌΠ΅Ρ‚Π°Π΄Π°Π½Π½Ρ‹Π΅ события
  • ΠΌΠ΅Ρ‚ΠΎΠ΄ Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ suspend (Π² ΠΏΡ€ΠΈΠ½Ρ†ΠΈΠΏΠ΅, это ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΈΠ΅ ΠΌΠΎΠΆΠ½ΠΎ ΡΠ½ΡΡ‚ΡŒ, Π½ΠΎ сСйчас Ρ‚Π°ΠΊ Π² Π΄Π²ΠΈΠΆΠΊΠ΅ ΠΈ Π½Π΅ ΠΏΠ»Π°Π½ΠΈΡ€ΡƒΡŽ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π½Π΅ suspend-ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹)
  • ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ Π² Ρ€Π΅Π°ΠΊΡ‚ΠΎΡ€Π΅ НЕ ΠΎΡ‚ΠΌΠ΅Π½ΠΈΡ‚ сохранСниС события ΠΈ запуск Π΄Ρ€ΡƒΠ³ΠΈΡ… Ρ€Π΅Π°ΠΊΡ‚ΠΎΡ€ΠΎΠ²

Π§Ρ‚Π΅Π½ΠΈΠ΅ Π΄Π°Π½Π½Ρ‹Ρ…

Π§Ρ‚Π΅Π½ΠΈΠ΅ Π΄Π°Π½Π½Ρ‹Ρ… основной ΠΏΡ€ΠΎΠ΅ΠΊΡ†ΠΈΠΈ -- Π½ΠΈΠΊΠ°ΠΊΠΈΡ… ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΈΠΉ, ΠΊΠ°ΠΊ ΠΎΠ±Ρ‹Ρ‡Π½ΠΎ.

Π’Π°ΠΊ ΠΆΠ΅ доступно Ρ‡Ρ‚Π΅Π½ΠΈΠ΅ событий:

interface EventStoreReader {

  fun <T : DomainEvent> findEventsSinceId(
    eventIdFrom: Long,
    aggregator: String? = null,
    aggregatorGuid: UUID? = null,
    accountGuid: AccountGuid? = null,
    eventTypes: List<String>? = null,
    maxBatchSize: Int? = null,
  ): Flow<DomainEventWithIdAndMeta<T>>

  fun <T : DomainEvent> findEventsSinceGuid(
    eventGuidFrom: UUID,
    aggregator: String? = null,
    aggregatorGuid: UUID? = null,
    accountGuid: AccountGuid? = null,
    eventTypes: List<String>? = null,
    maxBatchSize: Int? = null,
  ): Flow<DomainEventWithIdAndMeta<T>>

  fun <T : DomainEvent> findEventsSinceDate(
    date: LocalDateTime,
    aggregator: String? = null,
    aggregatorGuid: UUID? = null,
    accountGuid: AccountGuid? = null,
    eventTypes: List<String>? = null,
    maxBatchSize: Int? = null,
  ): Flow<DomainEventWithIdAndMeta<T>>

  fun <T : DomainEvent> findEvents(
    aggregator: String? = null,
    aggregatorGuid: UUID? = null,
    accountGuid: AccountGuid? = null,
    eventTypes: List<String>? = null,
    maxBatchSize: Int? = null,
  ): Flow<DomainEventWithIdAndMeta<T>>

}

Π­Ρ‚ΠΎ API ΠΌΠΎΠΆΠ½ΠΎ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ для получСния истории ΠΈΠ»ΠΈ для создания асинхронных ΠΏΡ€ΠΎΠ΅ΠΊΡ†ΠΈΠΉ.

ΠŸΠΎΡ‚Π΅Π½Ρ†ΠΈΠ°Π»ΡŒΠ½ΠΎ ΠΌΠΎΠΆΠ½ΠΎ Π½Π°ΠΏΠΈΡΠ°Ρ‚ΡŒ ΠΈ своС API чтСния событий, Π² jOOQ всС для этого Π΅ΡΡ‚ΡŒ.

Π’Π°ΠΊ ΠΆΠ΅ ΠΌΠΎΠΆΠ½ΠΎ Π΄Π΅Π»Π°Ρ‚ΡŒ ΠΏΠΎΠ»Π½ΡƒΡŽ ΠΈΠ»ΠΈ Ρ‡Π°ΡΡ‚ΠΈΡ‡Π½ΡƒΡŽ ΠΏΠ΅Ρ€Π΅Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΡŽ Π±Π°Π·Ρ‹ (Π°Ρ€Π³ΡƒΠΌΠ΅Π½Ρ‚Ρ‹ старта прилоТСния ΠΈΠ»ΠΈ кастомный ΠΊΠΎΠ΄).

ΠŸΡ€ΠΈΠΌΠ΅Ρ€ получСния истории (СстСствСнно, ΠΌΠΎΠΆΠ½ΠΎ ΡΠΌΠ΅ΡˆΠΈΠ²Π°Ρ‚ΡŒ Ρ‡Ρ‚Π΅Π½ΠΈΠ΅ ΠΈΠ· событий ΠΈ ΠΈΠ· основной ΠΏΡ€ΠΎΠ΅ΠΊΡ†ΠΈΠΈ, Ρ‚.ΠΊ. это всС Π² Π΄Π°ΠΆΠ΅ ΠΎΠ΄Π½ΠΎΠΉ Π±Π°Π·Π΅):

@Service
class DebugService(
  private val eventStoreReader: EventStoreReader,
) {
  suspend fun getUserAudit(userGuid: UUID): List<String> {
    return eventStoreReader.findEvents<UserEvent>("user", userGuid, maxBatchSize = 100)
      .map { (id, event, meta) ->
        when (event) {
          is UserMetaUpdated -> "updated $event"
          is UserRegistered -> "user registered with id $id ${meta.createdAt} $event"
          is UserRemoved -> "user deleted at ${meta.createdAt}"
        }
    }
  }
}

Π’ΡƒΡ‚ Π² API Π½Π΅ΠΌΠ½ΠΎΠ³ΠΎ нСкрасиво -- Π½Π΅Ρ‚ связи "user" ΠΈ UserEvent. Π’ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎ, ΠΈΠΌΠ΅Π΅Ρ‚ смысл ΠΏΠ΅Ρ€Π΅Π΄Π°Π²Π°Ρ‚ΡŒ Π±Π°Π·ΠΎΠ²Ρ‹ΠΉ класс, Π½ΠΎ ΠΎΠ½ абстрактный. Если Ρƒ ΠΊΠΎΠ³ΠΎ-Ρ‚ΠΎ Π΅ΡΡ‚ΡŒ ΠΈΠ΄Π΅ΠΈ ΠΊΠ°ΠΊ Π»ΡƒΡ‡ΡˆΠ΅ ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ API (Π±Π΅Π· строчки "user" ΠΈ Π±Π΅Π· привСдСния "as UserEvent") -- Π±ΡƒΠ΄ΡƒΡ‚ Ρ€Π°Π΄ ΠΏΡ€ΠΎΡ‡ΠΈΡ‚Π°Ρ‚ΡŒ.

ΠžΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΈΡ

  • Π’ Π΄Π°Π½Π½ΠΎΠΉ Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ Event Bus Π½Π΅ Π²Π½Π΅Π΄Ρ€Π΅Π½ (для трансляции событий Ρ‡Π΅Ρ€Π΅Π· ΠΊΠ°ΠΊΡƒΡŽ-Π½ΠΈΠ±ΡƒΠ΄ΡŒ ΠšΠ°Ρ„ΠΊΡƒ ΠΈΠ»ΠΈ NATS), Π½ΠΎ Π½ΠΈΡ‡Π΅Π³ΠΎ Π½Π΅ ΠΌΠ΅ΡˆΠ°Π΅Ρ‚ Ρ‚Π°ΠΊΠΎΠ΅ ΠΏΡ€ΠΈΠΊΡ€ΡƒΡ‚ΠΈΡ‚ΡŒ, Ссли ΠΊΠΎΠΌΡƒ-Π½ΠΈΠ±ΡƒΠ΄ΡŒ Π±ΡƒΠ΄Π΅Ρ‚ Π½ΡƒΠΆΠ½ΠΎ.

Π˜Ρ‚ΠΎΠ³

Кода Π½Π΅ΠΌΠ½ΠΎΠ³ΠΎ большС Π·Π° счСт выдСлСния ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠΉ абстракции -- Π‘ΠΎΠ±Ρ‹Ρ‚ΠΈΠ΅. Π’Π°ΠΊ ΠΆΠ΅ врСмя ΡƒΡ…ΠΎΠ΄ΠΈΡ‚ Π½Π° саму Π°Π±ΡΡ‚Ρ€Π°ΠΊΡ†ΠΈΡŽ -- Π½Π°Π·Π²Π°Ρ‚ΡŒ, Π²Ρ‹Π΄Π΅Π»ΠΈΡ‚ΡŒ поля ΠΈ Ρ‚.ΠΏ.

Для CRUD получаСтся большС ΠΊΠΎΠ΄Π°, Π½ΠΎ ΠΊΡ€ΡƒΠ΄Π° Π½Π΅ Ρ‚Π°ΠΊ ΠΌΠ½ΠΎΠ³ΠΎ ΠΊΠ°ΠΊ ΠΌΠΎΠΆΠ΅Ρ‚ ΠΏΠΎΠΊΠ°Π·Π°Ρ‚ΡŒΡΡ -- Π½ΡƒΠΆΠ½ΠΎ ΠΏΡ€ΠΈΡƒΡ‡ΠΈΡ‚ΡŒ сСбя Π΄ΡƒΠΌΠ°Ρ‚ΡŒ Π² событиях бизнСс-области, Π° Π½Π΅ ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ/ΡƒΠ΄Π°Π»ΠΈΡ‚ΡŒ запись Π² Ρ‚Π°Π±Π»ΠΈΡ†Π΅ Π±Π°Π·Ρ‹ Π΄Π°Π½Π½Ρ‹Ρ….

Π’ Ρ†Π΅Π»ΠΎΠΌ, ΠΌΠ½Π΅ нравится, поэтому ΠΈ Ρ€Π΅ΡˆΠΈΠ» ΠΏΠΎΠ΄Π΅Π»ΠΈΡ‚ΡŒΡΡ с сообщСством.

Tech stack

  • Kotlin 1.8
  • Spring Boot 3 (reactive with Kotlin co-routines)
  • Spring Data Repositories & jOOQ
  • JUnit 5 with mockk
  • Java 17
  • Postgres
  • Docker

Dev links

Running the application in dev mode

You can run your application in dev mode that enables live coding using:

./bin/start-postgres
./bin/generate-flyway
./bin/generate-jooq
./bin/run-dev

Packaging and running the application

The application can be packaged using:

./bin/build-docker

About

Kotlin event sourcing app template (with ES engine)

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published