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

Events-First Microservices with Lagom

Events-First Microservices with Lagom

Even with microservices starting to become a commodity, a lot of implementations still fail as microliths. To ensure the autonomy of services and its development process, strong coupling between services should be limited as much as possible. The way to achieve this is to isolate the inner working of a service at cost and to switch from a commanding and synchronous to a promising and asynchronous way of communication between services.

A route to success is to stop architectural design from a perspective of services with well-defined and canonical relationships and to switch to a model which is based on the production of immutable domain events. Not only does this way of architecting ensure a better match with the things that happen within your business. The focus on the timeline of facts over the structures producing them ensures far less coupling within your system.

In this talk, I will share how Lagom embraces this way of thinking by pushing events-first development as a default. From the event-sourcing principles within aggregates to the message-broker functionality between services, I’ll give you the background of the elements which are in place to shift your system to an event-driven microservice architecture.

Kenny Baas-Schwegler

April 17, 2018
Tweet

More Decks by Kenny Baas-Schwegler

Other Decks in Technology

Transcript

  1. 2 IT’S YOUR BUSINESS. WE ACCELERATE IT. XEBIA GROUP CONSISTS

    OF SEVEN SPECIALIZED, INTERLINKED COMPANIES WITH OFFICES IN AMSTERDAM AND HILVERSUM (NETHERLANDS), PARIS, DELHI, BANGALORE AND BOSTON, WE EMPLOY OVER 700 PEOPLE WORLDWIDE
  2. “Any organization that designs a system will produce a design

    whose structure is a copy of the organization's communication structure.”
 
 — Melvin Conway, 1967
  3. We not only need boundaries, We need an architectural pattern

    which enable autonomy within and over those boundaries
  4. Design is not just what it looks like and feels

    like. Design is how it works.
  5. It is not the things that matter (in early stages

    of design). It is the things that happen — Russ Miles
  6. package com.xebia.webshop.order.api sealed trait OrderEvent object OrderEvent { case class

    OrderCreated(orderId: String, orderLines: Seq[OrderLine], shippingInfo: ShippingInfo) extends OrderEvent case class OrderConfirmed(orderId: String) extends OrderEvent case class OrderPaid(orderId: String, orderLines: Seq[OrderLine], shippingInfo: ShippingInfo) extends OrderEvent object OrderCreated { implicit val format: Format[OrderCreated] = Json.format } object OrderConfirmed { implicit val format: Format[OrderConfirmed] = Json.format } object OrderPaid { implicit val format: Format[OrderPaid] = Json.format } implicit val format: Format[OrderEvent] = derived.flat.oformat((__ \ "type").format[String]) }
  7. package com.xebia.webshop.shipping.api sealed trait OrderEvent object OrderEvent { case class

    OrderPicked(orderId: String, pickedBy: Employee) extends OrderEvent case class OrderShipped(orderId: String, shippingReference: String) extends OrderEvent case class OrderDelivered(orderId: String, wasDeliveredAt: LocalDateTime) extends OrderEvent object OrderPicked { implicit val format: Format[OrderPicked] = Json.format } object OrderShipped { implicit val format: Format[OrderShipped] = Json.format } object OrderDelivered { implicit val format: Format[OrderDelivered] = Json.format } implicit val format: Format[OrderEvent] = derived.flat.oformat((__ \ "type").format[String]) }
  8. Lagom provides an opinionated way of building microservices that intentionally

    constrains what a developer can do and how they should do it.
  9. package com.xebia.webshop.order.api import akka.NotUsed import com.lightbend.lagom.scaladsl.api.broker.Topic import com.lightbend.lagom.scaladsl.api.transport._ import com.lightbend.lagom.scaladsl.api.{Descriptor,

    Service, ServiceCall} trait OrderService extends Service { import Method._ import Service._ def orderEvents: Topic[OrderEvent] final override def descriptor = { named("order").withTopics( topic("order-OrderEvent", orderEvents) ) } }
  10. package com.xebia.webshop.order.api import akka.NotUsed import com.lightbend.lagom.scaladsl.api.broker.Topic import com.lightbend.lagom.scaladsl.api.transport._ import com.lightbend.lagom.scaladsl.api.{Descriptor,

    Service, ServiceCall} trait OrderService extends Service { import Method._ import Service._ def orderEvents: Topic[OrderEvent] final override def descriptor = { named("order").withTopics( topic("order-OrderEvent", orderEvents) ) } }
  11. Order Order Picked Whenever an Order is Paid Pick Order

    Order Order Paid Pay Order Order Shipping
  12. trait POrderCommand object POrderCommand { case class CreateOrder(orderLines: Seq[POrderLine], info:

    PShippingInfo) extends POrderCommand with ReplyType[Done] case class PayOrder(reference: String) extends POrderCommand with ReplyType[Done] case object GetOrder extends POrderCommand with ReplyType[POrder] }
  13. trait POrderEvent extends AggregateEvent[POrderEvent] { override def aggregateTag: AggregateEventTagger[POrderEvent] =

    POrderEvent.Tag } object POrderEvent { val Tag = AggregateEventTag[POrderEvent] case class OrderCreated( orderLines: Seq[POrderLine], shippingInformation: PShippingInfo) extends POrderEvent case object OrderConfirmed extends POrderEvent case class OrderPaid(reference: String) extends POrderEvent }
  14. case class POrder(id: String, orderLines: Seq[POrderLine], shippingInfo: Option[PShippingInfo], paymentReference: Option[String],

    confirmed: Boolean) object POrder { case class PShippingInfo(address: String, zipcode: String, city: String, country: String) case class PSKU(id: String) case class POrderLine(sku: PSKU, amount: Int) }
  15. class OrderEntity extends PersistentEntity { override type Command = POrderCommand

    override type Event = POrderEvent override type State = POrder override def initialState = POrder(this.entityId, Seq.empty, None, None, false) override def behavior: Behavior = Actions() .onCommand[POrderCommand.CreateOrder, Done] { case (POrderCommand.CreateOrder(orderLines, shippingInfo), ctx, state) => { ctx.thenPersist(POrderEvent.OrderCreated(orderLines, shippingInfo)) { evt => ctx.reply(Done) } } }.onCommand[POrderCommand.PayOrder, Done] { case (POrderCommand.PayOrder(reference), ctx, state) => { ctx.thenPersistAll(POrderEvent.OrderPaid(reference), POrderEvent.OrderConfirmed) { () => ctx.reply(Done) } } }.onReadOnlyCommand[GetOrder.type, POrder] { case (GetOrder, ctx, state) => ctx.reply(state) }.onEvent { case (POrderEvent.OrderCreated(lines, shippingInfo), state) => state.copy(orderLines = lines, shippingInfo = Some(shippingInfo)) case (POrderEvent.OrderPaid(reference), state) => state.copy(paymentReference = Some(reference)) case (POrderEvent.OrderConfirmed, state) => state.copy(confirmed = true) } }
  16. class OrderEntity extends PersistentEntity { override type Command = POrderCommand

    override type Event = POrderEvent override type State = POrder override def initialState = POrder(this.entityId, Seq.empty, None, None, false) override def behavior: Behavior = Actions() .onCommand[POrderCommand.CreateOrder, Done] { case (POrderCommand.CreateOrder(orderLines, shippingInfo), ctx, state) => { ctx.thenPersist(POrderEvent.OrderCreated(orderLines, shippingInfo)) { evt => ctx.reply(Done) } } }.onCommand[POrderCommand.PayOrder, Done] { case (POrderCommand.PayOrder(reference), ctx, state) => { ctx.thenPersistAll(POrderEvent.OrderPaid(reference), POrderEvent.OrderConfirmed) { () => ctx.reply(Done) } } }.onReadOnlyCommand[GetOrder.type, POrder] { case (GetOrder, ctx, state) => ctx.reply(state) }.onEvent { case (POrderEvent.OrderCreated(lines, shippingInfo), state) => state.copy(orderLines = lines, shippingInfo = Some(shippingInfo)) case (POrderEvent.OrderPaid(reference), state) => state.copy(paymentReference = Some(reference)) case (POrderEvent.OrderConfirmed, state) => state.copy(confirmed = true) } }
  17. class OrderEntity extends PersistentEntity { override type Command = POrderCommand

    override type Event = POrderEvent override type State = POrder override def initialState = POrder(this.entityId, Seq.empty, None, None, false) override def behavior: Behavior = Actions() .onCommand[POrderCommand.CreateOrder, Done] { case (POrderCommand.CreateOrder(orderLines, shippingInfo), ctx, state) => { ctx.thenPersist(POrderEvent.OrderCreated(orderLines, shippingInfo)) { evt => ctx.reply(Done) } } }.onCommand[POrderCommand.PayOrder, Done] { case (POrderCommand.PayOrder(reference), ctx, state) => { ctx.thenPersistAll(POrderEvent.OrderPaid(reference), POrderEvent.OrderConfirmed) { () => ctx.reply(Done) } } }.onReadOnlyCommand[GetOrder.type, POrder] { case (GetOrder, ctx, state) => ctx.reply(state) }.onEvent { case (POrderEvent.OrderCreated(lines, shippingInfo), state) => state.copy(orderLines = lines, shippingInfo = Some(shippingInfo)) case (POrderEvent.OrderPaid(reference), state) => state.copy(paymentReference = Some(reference)) case (POrderEvent.OrderConfirmed, state) => state.copy(confirmed = true) } }
  18. class OrderEntity extends PersistentEntity { override type Command = POrderCommand

    override type Event = POrderEvent override type State = POrder override def initialState = POrder(this.entityId, Seq.empty, None, None, false) override def behavior: Behavior = Actions() .onCommand[POrderCommand.CreateOrder, Done] { case (POrderCommand.CreateOrder(orderLines, shippingInfo), ctx, state) => { ctx.thenPersist(POrderEvent.OrderCreated(orderLines, shippingInfo)) { evt => ctx.reply(Done) } } }.onCommand[POrderCommand.PayOrder, Done] { case (POrderCommand.PayOrder(reference), ctx, state) => { ctx.thenPersistAll(POrderEvent.OrderPaid(reference), POrderEvent.OrderConfirmed) { () => ctx.reply(Done) } } }.onReadOnlyCommand[GetOrder.type, POrder] { case (GetOrder, ctx, state) => ctx.reply(state) }.onEvent { case (POrderEvent.OrderCreated(lines, shippingInfo), state) => state.copy(orderLines = lines, shippingInfo = Some(shippingInfo)) case (POrderEvent.OrderPaid(reference), state) => state.copy(paymentReference = Some(reference)) case (POrderEvent.OrderConfirmed, state) => state.copy(confirmed = true) } }
  19. Event Processor OrderConfirmed OrderPaid OrderChanged OrderCreated Repository (Payment => Order)

    ID: A ID: B ID: C OrderConfirmed OrderPaid OrderChanged OrderCreated OrderConfirmed OrderPaid OrderChanged OrderCreated
  20. Event Processor SEQ: 12 #1 #2 #3 #4 #5 #6

    #7 #8 #9 #10 #11 #12 SEQ: 0 ID: A ID: B ID: C OrderConfirmed OrderPaid OrderChanged OrderCreated OrderConfirmed OrderPaid OrderChanged OrderCreated OrderConfirmed OrderPaid OrderChanged OrderCreated Repository (Payment => Order)
  21. Event Processor SEQ: 12 #1 #2 #3 #4 #5 #6

    #7 #8 #9 #10 #11 #12 SEQ: 12 ID: A ID: B ID: C OrderConfirmed OrderPaid OrderChanged OrderCreated OrderConfirmed OrderPaid OrderChanged OrderCreated OrderConfirmed OrderPaid OrderChanged OrderCreated Repository (Payment => Order)
  22. class OrderServiceImpl(persistentEntityRegistry: PersistentEntityRegistry)(implicit ec: ExecutionContext) extends OrderService { override def

    payOrder(orderId: String): ServiceCall[ConfirmOrderRequest, NotUsed] = ServiceCall { req => persistentEntityRegistry.refFor[OrderEntity](orderId).ask(POrderCommand.PayOrder(req.paymentReference)).map(_ => NotUsed) } override def orderEvents = TopicProducer.singleStreamWithOffset { offset => persistentEntityRegistry.eventStream(POrderEvent.Tag, offset).filter(e => /* Filter some of the internal events to isolate some of the entities internal logic */ e.event.isInstanceOf[POrderEvent.OrderCreated] || e.event == POrderEvent.OrderPaid ).mapAsync(1) { event => event.event match { case POrderEvent.OrderCreated(orderLines, shippingInfo) => val message = api.OrderEvent.OrderCreated(event.entityId, orderLines.map(x => api.Order.OrderLine(api.Order.SKU(x.sku.id), x.amount)), ShippingInfo(shippingInfo.address, shippingInfo.zipcode, shippingInfo.city, shippingInfo.country)) Future.successful((message, event.offset)) case x: POrderEvent.OrderPaid => persistentEntityRegistry.refFor[OrderEntity](event.entityId).ask(POrderCommand.GetOrder).map { x => val shippingInfo = x.shippingInfo.getOrElse(throw new Exception("Shipping info is expected to be set when the order is confirmed")) val message = api.OrderEvent.OrderPaid( orderId = event.entityId, orderLines = x.orderLines.map(x => api.Order.OrderLine(api.Order.SKU(x.sku.id), x.amount)), shippingInfo = api.Order.ShippingInfo(shippingInfo.address, shippingInfo.zipcode, shippingInfo.city, shippingInfo.country) ) (message, event.offset) } } } } }
  23. Order Order Picked Whenever an Order is Paid Pick Order

    Order Order Paid Pay Order Order Shipping
  24. class OrderServicePolicy(persistentEntityRegistry: PersistentEntityRegistry, orderService: OrderService) { orderService.orderEvents.subscribe.atLeastOnce(Flow[OrderEvent].mapAsync(1) { case x:

    OrderPaid => entityRef(x.orderId).ask( PickOrder(x.orderId, PAddress(x.shippingInfo.address, x.shippingInfo.city, x.shippingInfo.address, x.shippingInfo.zipcode) )) case other => Future.successful(Done) }) private def entityRef(orderId: UUID) = persistentEntityRegistry.refFor[POrder](orderId.toString) }
  25. In this, we can treat REST as an anti-pattern for

    domain to domain communication
  26. As long as events are readable on a topic, the

    origin is of less importance
  27. Events-first delivers on a better way of modelling businesses in

    technology, while getting the rest for free
  28. Q&A