Slide 1

Slide 1 text

Pattern of Implement Domain Object with Akka Persistence Typed 2022.3.19 ScalaMatsuri 2022 TSUJI Yohei "LLB 1FSTJTUFODF5ZQFEʹ͓͚ΔυϝΠϯΦϒδΣΫτ ͷ࣮૷ύλʔϯ

Slide 2

Slide 2 text

2 TSUJI Yohei Scala Engineer at Chatwork Co., Ltd. Scala DDD(Domain Driven Design) Microservice Architecture and Reactive Architecture(Akka) We are working re-architecting our service with Akka. Twitter / GitHub @crossroad0201 "LLBΛ࢖ͬͨαʔϏεͷٕज़࡮৽ʹऔΓ૊ΜͰ͍·͢ɻ 2

Slide 3

Slide 3 text

Contents • The Akka Persistence Typed • The Domain Object • Integrate the Akka Persistence Typed and the Domain Object • Conclusion 3 ·ͣ"LLB 1FSTJTUFODF5ZQFEͱυϝΠϯΦϒδΣΫτͷ ཁ݅ʹ৮Ε͔ͯΒɺͦΕΒͷ౷߹ํ๏Λ঺հ͠·͢ɻ

Slide 4

Slide 4 text

4 The Akka Persistence Typed

Slide 5

Slide 5 text

Summary of Akka Persistence Typed • Akka sub-module for persisting Actors. • Event Sourcing. Not persists state of Actor, persists EVENTs that created by domain logic. • Insert EVENTs only. No update, No locks, high performance and scalability. • Can implement CQRS and Event-driven architecture by streaming events. ΠϕϯτιʔγϯάͰΞΫλʔΛӬଓԽ͠·͢ɻ ӬଓԽ͞ΕΔͷ͸ঢ়ଶͦͷ΋ͷͰ͸ͳ͘ɺΠϕϯτͰ͢ɻ 5 Persistent Actor Journal Insert EVENT Insert EVENT

Slide 6

Slide 6 text

Implementation constraints • Needs to be implement 2 callback functions for processing a message, called the Command Handler and Event Handler. ̍ͭͷϝοηʔδͷॲཧΛɺίϚϯυϋϯυϥͱΠϕϯτ ϋϯυϥʹ෼͚ͯॲཧΛ࣮૷͢Δඞཁ͕͋Γ·͢ɻ 6 object ExamplePersistentBehavior { def apply(…): Behavior[…] = EventSourcedBehavior( commandHandler, eventHandler ) val commandHandler: (State, Command) => Effect[Event, State] = ??? val eventHandler: (State, Event) => State = ??? }

Slide 7

Slide 7 text

What to do at the callbacks • The Command Handler • Do domain logic. (may fail) • Create EVENTs with result of domain logic and persists it. • (If necessary) Reply to caller. • The Event Handler • Update the state of Actor using created EVENTs. • NO fail, NO side effects. 7 ίϚϯυϋϯυϥͰ͸υϝΠϯϩδοΫΛ࣮ߦͯ͠Πϕϯ τΛੜ੒͠ɺΠϕϯτϋϯυϥͰঢ়ଶΛߋ৽͠·͢ɻ

Slide 8

Slide 8 text

8 The Domain Object

Slide 9

Slide 9 text

What to do at the Domain Object 1. Check invariants with latest state. 2. Do domain logic. 3. (If Event-driven architecture) Create an EVENTs. 4. Update the state to latest. 9 ෆม৚݅ΛνΣοΫͯ͠υϝΠϯϩδοΫΛ࣮ߦ͠ɺΠϕ ϯτΛੜ੒͢Δͱڞʹঢ়ଶΛ࠷৽ʹߋ৽͠·͢ɻ

Slide 10

Slide 10 text

What is the ideal Domain Object • Encapsulate knowledge about the Business domain. • Test it easily. • NOT depends specific technology. (just like Akka Persistence Typed) • (If Event-driven architecture) Define and create EVENTs in the domain layer. 10 υϝΠϯΦϒδΣΫτ͸ɺಛఆٕज़ͷӨڹΛड͚ͣʹ७ਮ ʹϏδωευϝΠϯͷ஌ࣝΛΧϓηϧԽ͢Δͷ͕ཧ૝Ͱ͢

Slide 11

Slide 11 text

11 Integrate the Persistent Actor and the Domain Object

Slide 12

Slide 12 text

Where to do, where to implement 1. Check invariants with latest state. 2. Do domain logic. 3. (If Event-driven architecture) Create an EVENTs. 4. Update the state to latest. 12 υϝΠϯΦϒδΣΫτͰߦ͏खଓ͖΋ɺίϚϯυϋϯυϥ ͱΠϕϯτϋϯυϥʹରԠͤ͞ͳ͚Ε͹ͳΓ·ͤΜɻ in Command Handler In Event Handler

Slide 13

Slide 13 text

Implementation styles Introduce 3 styles. See the example code for more details. https://github.com/crossroad0201/ddd-with-akka-persistence-typed • Style A -> example1 • Style B -> example3 • Style C -> example5 13 ͜͜Ͱ͸̏ͭͷ࣮૷ελΠϧΛ঺հ͠·͢ɻ (JU)VCͷαϯϓϧίʔυ΋͋Θͤͯࢀর͍ͩ͘͞ɻ

Slide 14

Slide 14 text

Implementation style A γϯϓϧͳ࣮૷Ͱ͕͢ɺυϝΠϯϩδοΫͱΠϕϯτͷఆ ٛੜ੒͕ΞΫλʔଆʹ࣮૷͞Ε·͢ɻ 14 package example1.domain case class Task(…, subject: Subject, status: Status) { def canEditSubject: Boolean = status != Status.Done def editSubject(newSubject: Subject): Task = { require(canEditSubject) copy(subject = newSubject) } package example1.interfaceadapter object TaskPersistenceBehavior { def commandHandler(…) = case (Just(entity), EditSubject(subject, replyTo)) => if (entity.canEditSubject) Effect.persist(SubjectEdited(subject)) .thenReply(replyTo)(_ => EditSubjectSucceeded) def eventHandler(...) = case (Just(entity), SubjectEdited(newSubject)) => Just(entity.editSubject(newSubject)) case class SubjectEdited(newSubject: Subject) extends TaskEvent Domain Actor (1) Check invariants Domain (2) Do domain logic Actor (3) Define EVENTs Actor (4) Create EVENT Actor (5) Update state Domain (3) (4) (1) (2) (5)

Slide 15

Slide 15 text

Implementation style B υϝΠϯϩδοΫͷ࣮ߦͱΠϕϯτͷੜ੒ΛɺυϝΠϯΦ ϒδΣΫτʹΧϓηϧԽͰ͖·ͨ͠ɻ 15 package example3.domain case class Task(…, subject: Subject, status: Status) { def editSubject(newSubject: Subject) : Either[…] = if (status == Status.Todo) Right(SubjectEdited(newSubject)) def applyEvent(event: TaskMutationEvent) = event match { case SubjectEdited(newSubject) => copy(subject = newSubject) case class SubjectEdited(newSubject: Subject) extends TaskMutationEvent package example3.interfaceadapter object TaskPersistenceBehavior { def commandHandler(…) = case (Just(entity), EditSubject(newSubject, replyTo)) => entity.editSubject(newSubject) match { case Right(event) => Effect.persist(event) .thenReply(replyTo)(_ => EditSubjectSucceeded) def eventHandler(...) = case (Just(entity), event: TaskMutationEvent) => Just(entity.applyEvent(event)) Domain Actor (1) Check invariants Domain (2) Do domain logic Domain (3) Define EVENTs Domain (4) Create EVENT Domain (5) Update state Domain (1), (2) (3) (4) (5)

Slide 16

Slide 16 text

Implementation style C ͞Βʹൃల࣮ͤͨ͞૷ྫɻυϝΠϯϩδοΫ͕ฦ݁͢Ռܕ ͔Β࠷৽ͷঢ়ଶΛऔಘͰ͖ɺςετ༰қੑΛߴΊ·͢ɻ 16 package example5.domain trait Result[EVENT, ENTITY] { val event: EVENT def entity: ENTITY } case class Task(…, subject: Subject, state: State) { def editSubject(newSubject: Subject): Either[…] = if (status == Status.Todo) { Right(Result(SubjectEdited(newSubject), Task.this)) case class SubjectEdited(newSubject: Subject) extends TaskMutationEvent { override def playTo(entity: Task): Task = entity.copy(subject = newSubject) package example5.interfaceadapter object TaskPersistenceBehavior { def commandHandler(…) = case (Just(entity), EditSubject(newSubject, replyTo)) => entity.editSubject(newSubject) match { case Right(result) => Effect.persist(result.event) .thenReply(replyTo)(_ => EditSubjectSucceeded) def eventHandler(...) = case (Just(entity), event: TaskMutationEvent) => Just(event.playTo(entity)) (1) Check invariants Domain (2) Do domain logic Domain (3) Define EVENTs Domain (4) Create EVENT Domain (5) Update state Domain ❗'enEty' will not used by Actor. No costs because lazy evaluaEon. Domain Actor (1), (2) (3) (4) (5)

Slide 17

Slide 17 text

Comparing test code for the Domain Object 17 val sut: Task = Task.create( …, Subject("Test"), … ) val actualChecked = sut.canEditSubject assert(actualChecked) // Can not test domain logic // and event creation in this test. val actualEntity = sut.editSubject(Subject("Edited")) assert(actualEntity == Task(…, Subject("Edited"), …) ) val sut: Task = Task.applyEvent( Task.create( …, Subject("Test"), … ) ) val actualEvent = sut.editSubject(Subject("Edited")) assert(actualEvent == Right(SubjectEdited(Subject("Edited"))) ) val actualEntity = sut.applyEvent(actualEvent) assert(actualEntity == Task(…, Subject("Edited"), …) ) val sut: Task = Task.create( …, Subject("Test"), … ) .entity val actual = sut.editSubject(Subject("Edited")) assert(actual.event == SubjectEdited(Subject("Edited")) ) assert(actual.entity == Task( …, Subject("Edited"), …) ) To create test target, needs create and apply an EVENT…🤔 Create test target easily!!👍 Style A Style B Style C

Slide 18

Slide 18 text

Conclusion • Encapsulate as many as possible to the Domain Objects. (Style B or C) • Testability for the Domain Objects is important for growth it. • If your business domain is very simple, there is also a way to implement everything to the Actor sate without separated domain layer. Ͱ͖Δ͚ͩଟ͘ΛυϝΠϯΦϒδΣΫτʹΧϓηϧԽ͠ɺ ୯ಠͰςετͰ͖ΔΑ͏ʹ͓ͯ͘͜͠ͱΛ͓͢͢Ί͠·͢ 18

Slide 19

Slide 19 text

19 19 Thank you for listening. Thank you to my team. ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ɻ