Krzysztof Borowski @liosedhel
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
case class WorldMap(
id: WorldMapId,
creatorId: UserId,
description: String
)
It is time to apply these design principles consciously from the start instead of rediscovering them each time.
Problems
Solutions
concepts: ReactiveX, Reactive Streams
ready to use: RxJava, RxKotlin, RxSwift, RxJS, Monix,
akka-streams, vert.x, Ratpack
Functional Reactive Programming?
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
akka ~> akka-persistence ~> lagom
akka ~> akka-cluster ~> akka-sharding ~> lagom
akka ~> akka-streams ~> akka-http ~> play ~> lagom ~> docker ~> kubernetes
kafka ~> lagom
(cassandra, postgres, ...) ~> lagom
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)
}
...
}
}
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)
)
// 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
)
)
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 _)
}
}
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("")
)
)
}
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
)
}
//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)
)
)
}
override def worldMapCreatedTopic():
Topic[WorldMapApiEvents.WorldMapCreated] = {
TopicProducer.singleStreamWithOffset { fromOffset =>
persistentEntityRegistry
.eventStream(WorldMapAggregate.WorldMapEvent.Tag, fromOffset)
.mapConcat(filterWorldMapCreated)
}
}
worldMapService
.worldMapCreatedTopic()
.subscribe
.atLeastOnce(
Flow[WorldMapCreated].map { worldMapCreated =>
analyticsRepository.save(worldMapCreated)
Done
}
)
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`)
A Service Locator is embedded in Lagom’s development environment, allowing services to discover and communicate with each other.
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.
Easy to run the whole project in Development Mode
just type ‘runAll’ and the whole project starts (including Cassandra, Kafka and all microservices)
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
Quite controversial design and delays
You have to remember: sometimes PersistenceEntity approach is simply wrong. Then you just have to use standard state-based DB approach.
If you don’t want to use Kubernetes since the very beginning - forget about Lagom
The preferred approach is that the ServiceCall is just a Future, so by default error handling is not type-safe.
As Greg Young says - you should NOT use CQRS frameworks :)
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.
Thank you :)
Feedback appreciated: https://goo.gl/euhFXD