Short Trip into Reactive System space with Lagom

Krzysztof Borowski @liosedhel

Trip plan

  • Domain Driven Design
  • Reactive Systems
  • Reactive Programming
  • ES and CQRS
  • Lagom 
  • Demo

PhotoTrip App

  • Mark on the map our trip
  • Add photos
  • Collaborate with others in real time
  • Add Comments

Domain Driven Design

Bounded Context

 

Bounded Context is a central pattern in Domain-Driven Design. It is the focus of DDD's strategic design section which is all about dealing with large models and teams. DDD deals with large models by dividing them into different Bounded Contexts and being explicit about their interrelationships.

Martin Fowler

Bounded Context

Aggregate

  • A cluster of domain objects that can be treated as a single unit.
  • Aggregates domain objects and theirs behaviour
  • Every change of the state within aggregate is transactional

Aggregates

Entity

  • Has unique identificator
  • Can mutate the state
  • Contains other entities or value objects
 case class WorldMap(
    id: WorldMapId, 
    creatorId: UserId,
    description: String
 ) 

Reactive Systems Space

Reactive System Definition

Systems spectrum

It is time to apply these design principles consciously from the start instead of rediscovering them each time.

Why should​ we care?

  • Our software operates 24/7 
  • Dealing with high load, responding in milliseconds
  • Solving more and more complicated domain problems
  • The codebase is growing, software is evolving quickly

Architecture Overview

Problems

  • Handling distributed architecture deployment
  • Defining proper service boundaries
  • Reliable messaging system
  • Data streams, asynchronous communication
  • Lack of well defined, tested in production frameworks
  • Domain Driven Design (~2003)
  • Persistent Messages Queues (Kafka (~2012))
  • Reactive Programming (~2007)
  • Lagom, Project Reactor (~2016)
  • Containers & Orchestration (~2013)

Problems

Solutions

Reactive Programming

Reactive Programming 

  • reacting to changes
  • often via data streams
  • program using and relying on events instead of the order of lines in the code

Using reactive programming tools does not imply having ​a Reactive System!

Reactive Programming Marketplace

concepts: ReactiveX, Reactive Streams

ready to use: RxJava, RxKotlin, RxSwift, RxJS, Monix,

akka-streams, vert.x, Ratpack

Functional Reactive Programming?

Event Sourcing (single-writer)

Command/Query Responsibility Segregation

with ES

an open source framework for building reactive microservice systems in Java or Scala.

focus on solving business problems instead of wiring services together

Lightbend Factory

                 akka ~> akka-persistence ~> lagom
    akka ~> akka-cluster ~> akka-sharding ~> lagom
akka ~> akka-streams ~> akka-http ~> play ~> lagom ~> docker ~> kubernetes
                                    kafka ~> lagom 
               (cassandra, postgres, ...) ~> lagom

Looking inside

Service internals

Aggregate - Write Side

class WorldMapAggregate(pubSubRegistry: PubSubRegistry) extends PersistentEntity {

  import WorldMapAggregate._

  override type Command = WorldMapCommand[_]
  override type Event   = WorldMapEvent

  override type State = WorldMapAggregate.WorldMapState

  override def initialState: WorldMapAggregate.WorldMapState = UninitializedWordMap

  override def behavior: Behavior = {
    case UninitializedWordMap =>
      Actions()
        .onCommand[CreateNewMap, Done] {
          case (CreateNewMap(id, creatorId, description), ctx, _) =>
            ctx.thenPersist(
              WorldMapCreated(id, creatorId, description)
            ) { _ => ctx.reply(Done) }
        }
        .onEvent {
          case (WorldMapCreated(id, creatorId, description), _) =>
            WorldMap(id, creatorId, description)
        }
    ...
  }
}

Processor - Read Side

class WorldMapEventProcessor(
  readSide: CassandraReadSide,
  worldMapsRepository: WorldMapsRepository
) extends ReadSideProcessor[WorldMapEvent] {

  override def buildHandler(): ReadSideProcessor.ReadSideHandler[WorldMapEvent] = {
    val builder = readSide.builder[WorldMapEvent]("worldmaps")
    builder
      .setGlobalPrepare(() => worldMapsRepository.createWorldMapsTable())
      .setEventHandler[WorldMapCreated](
        eventStreamElement => worldMapsRepository.saveMap(eventStreamElement.event)
      )
      .build()
  }
  override def aggregateTags = Set(WorldMapEvent.Tag)
}

 //register event processor
 readSide.register[WorldMapAggregate.WorldMapEvent](
    new WorldMapEventProcessor(cassandraReadSide, worldMapsRepository)
 )

Internal Pub-Sub


// send created place to internal topic
val place = Place(placeId, worldMapId, description, coordinates, photoLinks)
val topic = pubSubRegistry.refFor(TopicId[Place](worldMapId.id))
topic.publish(place)


// subscribing
val worldMapId = WorldMapId(mapId)
val topic      = pubSub.refFor(TopicId[PlaceAggregate.Place](worldMapId.id))
topic
  .subscriber //akka-streams
  .map(
    p =>
      WorldMapApiModel.Place(
        p.id,
        p.description,
        p.coordinates,
        p.photoLinks
      )
  )
  

Going outside

Public APIs

Service API

trait WorldMapService extends Service {

  def worldMap(mapId: String): ServiceCall[NotUsed, WorldMap]

  def createWorldMap(): ServiceCall[NewWorldMap, Done]

  override def descriptor: Descriptor = {
    import Service._
    named("worldmap")
      .withCalls(
        pathCall("/api/world-map/map/:id", worldMap _),
        pathCall("/api/world-map/map", createWorldMap _)
  }
}

Service Implementation - Write Side

override def createWorldMap(): ServiceCall[WorldMapDetails, Done] =
    ServiceCall[WorldMapDetails, Done] { worldMap =>
      val worldMapAggregate =
        persistentEntityRegistry.refFor[WorldMapAggregate](worldMap.mapId.id)
      worldMapAggregate.ask(
        WorldMapAggregate.CreateNewMap(
          worldMap.mapId,
          worldMap.creatorId,
          worldMap.description.getOrElse("")
        )
      )
    }

Service Implementation - Read Side

  override def worldMap(mapId: String): ServiceCall[NotUsed, WorldMap] =
    ServiceCall { _ =>
      val worldMapId = WorldMapId(mapId)
      for {
        worldMap <- worldMapsRepository.getMap(worldMapId)
        places   <- placesRepository.getPlaces(worldMapId)
      } yield WorldMapApiModel.WorldMap(
                worldMapId, 
                worldMap.creatorId, 
                worldMap.description, 
                places
              )
    }

External topics

//topics available externally
def worldMapCreatedTopic(): Topic[WorldMapCreated]

override def descriptor: Descriptor = {
    import Service._
    named("worldmap")
      .withTopics(
        topic(WorldMapService.WORLD_MAP_CREATED, worldMapCreatedTopic())
          .addProperty(
            KafkaProperties.partitionKeyStrategy,
            PartitionKeyStrategy[WorldMapCreated](_.worldMapId.id)
          )
    )
}

External topic implementation

override def worldMapCreatedTopic(): 
    Topic[WorldMapApiEvents.WorldMapCreated] = {
    TopicProducer.singleStreamWithOffset { fromOffset =>
      persistentEntityRegistry
        .eventStream(WorldMapAggregate.WorldMapEvent.Tag, fromOffset)
        .mapConcat(filterWorldMapCreated)
    }
  }

External topics - subscribing

worldMapService
    .worldMapCreatedTopic()
    .subscribe
    .atLeastOnce(
      Flow[WorldMapCreated].map { worldMapCreated =>
        analyticsRepository.save(worldMapCreated)
        Done
      }
    )

Gluing services

lazy val `mytrip` = 
  (project in file("."))
    .aggregate(
      `mytrip-worldmap-api`,
      `mytrip-worldmap-impl`,
      ...
    )

lazy val `mytrip-worldmap-api` = 
  (project in file("mytrip-worldmap-api"))
    .settings(
      libraryDependencies ++= Seq(
        lagomScaladslApi,
        ...
      )
    )

lazy val `mytrip-worldmap-impl` = 
  (project in file("mytrip-worldmap-impl"))
    .enablePlugins(LagomScala)
    .settings(
      libraryDependencies ++= Seq(
        ...
      )
    )
    .dependsOn(`mytrip-worldmap-api`)

Service Locator

A Service Locator is embedded in Lagom’s development environment, allowing services to discover and communicate with each other.

API Gateway

External clients need a stable address to communicate to and here’s where the Service Gateway comes in. The Service Gateway will expose and reverse proxy all public endpoints registered by your services.

Lagom show time!

The Good Parts

Good Dev Environment

Easy to run the whole project in Development Mode

 just type ‘runAll’ and the whole project starts (including Cassandra, Kafka and all microservices)

Well-tested way

CQRS is a very general method. For a beginner it’s very hard to balance all possible solutions. Lagom provides a single, well tested way to do the staff. If you want to write Facebook from scratch, never made a CQRS system before and you want to keep it scalable, follow the Lagom’s way and it’ll just work.

Tomasz Pasternak

Very good microservices environment

  • Typesafe interfaces (both sync and async) - including LagomClient
  • Isolation between API and implementation
  • ServiceLocator (almost) for free
  • Easy to test on multiple levels

 

Week Points

Akka Persistence Cassandra implementation

Quite controversial design and delays

 

The only official approach is event sourcing

You have to remember: sometimes PersistenceEntity approach is simply wrong. Then you just have to use standard state-based DB approach.

The only official approach is event sourcing

Forces microservices approach

If you don’t want to use Kubernetes since the very beginning - forget about Lagom

Not a functional-style error handling

The preferred approach is that the ServiceCall is just a Future, so by default error handling is not type-safe.

It’s a framework

As Greg Young says - you should NOT use CQRS frameworks :)

GraphQL mismatch

Lagom strongly pushes you to strict Command-Query Separation. On the other hand, modern GraphQL APIs required to keep the possibility to get the new state of an aggregate in a response to a command.

What to do next

Inspiration

Thank you :)

Feedback appreciated: https://goo.gl/euhFXD

Short Trip into Reactive System space with Lagom

By liosedhel

Short Trip into Reactive System space with Lagom

  • 1,117