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
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
Command/Query Responsibility Segregation
with ES
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
an open source framework for building reactive microservice systems in Java or Scala.
focus on solving business problems instead of wiring services together
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!
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,489