Full Stack Reactive in Practice
Presenting a real-world CQRS
(command query responsibility
segregation) and ES (event
sourcing) architecture.
Kevin Webber
Director of Engineering @
Reading Plus
w: kevinwebber.ca
m: medium.com/@kvnwbbr
t: @kvnwbbr
— Reading Plus is an adaptive platform that helps K-12
students become successful readers and lifelong
learners
— We will be applying reactive techniques and
technologies to a mission with purpose
— Find us at https://readingplus.com and stay tuned for
upcoming engineering opportunities
Why Reactive?
— React, Vue, and other front-end frameworks are
enabling innovative user experiences
Why Reactive?
— React, Vue, and other front-end frameworks are
enabling innovative user experiences
— Meanwhile, relational databases continue to dominate
the server side
Why Reactive?
— React, Vue, and other front-end frameworks are
enabling innovative user experiences
— Meanwhile, relational databases continue to dominate
the server side
— Reactive principles and frameworks help create an
end-to-end event-driven channel, all the way up and
all the way down
Traditional architecture
— Unbounded in complexity and
side effects
— Putting React or Vue on top
won't help much
— Batch-mode, high-latency,
high-failure rates
— Has an impact
— Customer happiness?
— Developer joy?
— Visit TheDailyWTF :)
What we will cover
All references and examples will be in Java.
1. Designing reactive systems → Event Storming, DDD
2. Implementing reactive systems → Lagom, Play, Akka
3. Deploying reactive systems → sbt, Kubernetes
Part 1. Designing Reactive Systems
— Raw ingredients
— Commands & events
— Inception
— Event storming & DDD
— Fidelity
— Event modeling
— Handoff
— User stories
Systems have been event driven for centuries!
Events are
— Interesting things, that have
— happened in the past.
Event sourcing
— Commands are applied
against an entity, the entity
can accept or reject a
command
— If accepted, an entity will
generate an event and then
apply that event in order to
change state
Defining aggregate boundaries
— Structures implementation
around related items
— The yellow sticky in the
middle represents state
Defining bounded contexts
— Group together related
business concepts
— Contain domain complexity
— A single person can master an
aggregate
— A single team can master a
bounded context
How do we model a bounded context?
— Arrange events in linear order by time
— Events cluster into closely related groups (aggregates)
— Aggregates cluster into closely related groups
(bounded contexts)
— Event Storming helps discover the problem space
— Event Modeling creates a blueprint for a solution
From https://eventmodeling.org/posts/what-is-event-
modeling/
Essential learning resources
— Book: Event Storming by Alberto Brandolini
— Book: Domain-Driven Design Distilled by Vaughn
Vernon
— Website: Event Modeling by Adam Dymitruk
Part 2. Implementation
— Tools
— Lagom, Play, and Akka
— Cassandra and Kafka
— Pa!erns
— Event sourcing, CQRS
— Architecture
— Interface, BFF, services
— Integration
— Point to point, streaming,
pub/sub
Reactive Stock Trader
— Reference application for Reactive in Practice
— Vue, Lagom, Play, Akka, Cassandra, Kafka,
Kubernetes
— Bounded contexts
— Portfolio
— Broker
— Wire transfers
— https://github.com/RedElastic/reactive-stock-trader
Placing a wire transfer
We'll cover the command channel first.
1. Create command
2. Send over REST
3. Direct command to entity
4. Create event
5. Change state
6. Publish event
Submit transfer request (Vue.js)
onSubmit() {
this.submitted = true;
submitTransfer(this.form)
.then(() => {
this.submitted = false;
this.onReset();
});
},
API gateway
— Backend for Frontend (BFF) is
a pattern that involves
creating a 1:1 relationship
between backends and UIs
— The BFF then handles all calls
to various underlying
microservices (Portfolio,
Broker, Wires, etc)
— Also performs authentication,
authorization, etc
Play (command side API)
@Override
public ServiceCall<Transfer, TransferId> transferFunds() {
TransferId transferId = TransferId.newId();
return transfer ->
transferRepository
.get(transferId) // 1 (get reference to entity)
.ask(TransferCommand.TransferFunds.builder()
.source(transfer.getSourceAccount())
.destination(transfer.getDestinationAccount())
.amount(transfer.getFunds())
.build()
) // 2 (ask pattern)
.thenApply(done -> transferId);
}
BFF and Service Integration
— Play routes all commands and
queries to Lagom
microservices
— Lagom is a framework built on
top of Akka for developing
reactive microservices
— Based on the principles of
CQRS (Command Query
Responsibility Segregation)
Lagom (aggregates / entities)
— Based around persistent entities (aka, aggregates)
— Stored in memory for fast access
— Can be recovered via Cassandra as the event log
public class TransferEntity extends
PersistentEntity<TransferCommand,
TransferEvent,
Optional<TransferState>> {
// ...
}
Lagom (event sourcing)
— No mutable data, only
immutable state
— State is only changed on
successful event application
— Events are journaled
— Entity can always recover in-
memory by replaying events
from the journal
— Entities are not queried
directly, read-side queries are
backed by views
State machines
Let's say we're modeling an ATM
machine...
— This ATM machine has three
different states
— Each state has a different
behaviour for the same input
https://www.uml-diagrams.org/
state-machine-diagrams.html
Lagom (event sourcing)
private Behavior empty() {
BehaviorBuilder builder = newBehaviorBuilder(Optional.empty());
builder.setCommandHandler(TransferCommand.TransferFunds.class,
(cmd, ctx) -> { // 1 (handler is invoked from ask pattern)
TransferDetails transferDetails = TransferDetails.builder()
.source(cmd.getSource())
.destination(cmd.getDestination())
.amount(cmd.getAmount())
.build(); // 2 (build up details of xfer)
ObjectMapper mapper = new ObjectMapper();
TransferUpdated tc = buildTransferUpdated(
transferDetails, "Transfer Initiated");
publishedTopic.publish(
mapper.valueToTree(tc).toString()); // 3 (publish to stream)
return ctx.thenPersist(
new TransferEvent
.TransferInitiated(
getTransferId(), transferDetails),
evt -> ctx.reply(Done.getInstance())
); // 4 (persist event & return ID)
});
builder.setEventHandlerChangingBehavior(
TransferEvent.TransferInitiated.class,
this::fundsRequested); // 5 (change state)
return builder.build();
}
Views
— Read side processors build a
precomputed table
— Queries are only against
precomputed tables
— Use this to populate data on
initial page load
Creating a portfolio
1. When each portfolio is
created, we create a portfolio
entity and then persist the
create event to the journal
2. A read side processor
subscribes to journal updates
3. With each update, the read
side processor updates a
projected view
Let's cover #3...
Lagom ReadSide processor
Update a precomputed query table (Cassandra) on every
new event we subscribe to.
@Override
public ReadSideHandler<PortfolioEvent> buildHandler() {
return readSide.<PortfolioEvent>builder("portfolio_offset") // 1
.setGlobalPrepare(this::prepareCreateTables) // 2
.setPrepare(tag -> prepareWritePortfolios()) // 3
.setEventHandler(Opened.class, this::processPortfolioChanged) // 4
.build();
}
private CompletionStage<Done> prepareWritePortfolios() {
return session
.prepare("INSERT INTO portfolio_summary (portfolioId, name) VALUES (?, ?)")
.thenApply(ps -> {
this.writePortfolios = ps;
return Done.getInstance();
});
}
Queries are then executed against the projected view for
a vastly lower latency user experience.
@Override
public ServiceCall<NotUsed, PSequence<PortfolioSummary>> getAllPortfolios() {
return request -> {
CompletionStage<PSequence<PortfolioSummary>> result =
db.selectAll("SELECT portfolioId, name FROM portfolio_summary;")
.thenApply(rows -> {
List<PortfolioSummary> summary = rows.stream().map(row ->
PortfolioSummary.builder()
.portfolioId(new PortfolioId(
row.getString("portfolioId")))
.name(row.getString("name"))
.build())
.collect(Collectors.toList());
return TreePVector.from(summary);
});
return result;
};
}
Streaming
1. Render initial page with precomputed view over HTTP/
REST
2. Switch to unidirectional streaming for updates (events
over WS)
3. Commands over REST will still cause full page
refreshes (can change unidirectional stream to BiDi
stream in future)
Instead, let's use the Lagom PubSub API to push updates
to the UI in real-time...
Streaming (Lagom)
This code exposes a Reactive Streams Source via Lagom,
which Play can then "attach" to.
@Override
public ServiceCall<NotUsed, Source<String, ?>> transferStream() {
return request -> {
// subscribe to events on a specific topic ("transfer")
final PubSubRef<String> topic =
pubSub.refFor(
TopicId.of(String.class,
"transfer"));
// return the Source as a future (standard async Java 8)
return CompletableFuture.completedFuture(topic.subscriber());
};
}
Streaming (architecture)
PubSub works by broadcasting
events to subscribers:
— Publisher is TransferEntity
— Subscriber is
WireTransferServiceImpl
— This will create a streaming
Source
Create a streaming connection all the way down.
WireTransferController
public WebSocket ws() {
return WebSocket.Text.acceptOrResult(req -> {
return wireTransferService
.transferStream()
.invoke()
.thenApply(source -> {
return F.Either.Right(
Flow.fromSinkAndSource(
Sink.ignore(),
source
)
);
});
});
}
WebSockets (Vue.js)
connect() {
this.socket = new WebSocket(
"ws://localhost:9000/api/transfer/stream");
this.socket.onopen = () => {
this.socket.onmessage = (e) => {
let event = JSON.parse(e.data);
var index = -1;
// 1. determine if we're updating a row (initiated)
// or adding a new row (completed)
for (var i = 0; i < this.transfers.length; i++) {
if (this.transfers[i].id === event.id) {
index = i;
break;
}
}
if (index === -1) {
// unshift is similar to push, but prepends
this.transfers.unshift({
// ... 3. create object with id, status, etc
});
} else {
let t = {
// ... 4. create object with id, status, etc
};
this.transfers.splice(index, 1, t);
this.updateCashOnHand();
}
};
};
}
Part 3. Deployment
— Packaging
— sbt, service boundaries
— Testing
— Deploying to Minikube
— Handling dependencies in
k8s such as Cassandra,
Kafka
— Deployment
— Production deployments
Final architecture
Reactive Stock Trader needs...
— Formation of a cluster so Lagom entities can stay
available in memory
— Connection to Cassandra and Kafka (in a "high-
availability" clustered configuration)
— Cassandra for the backing journal
— Kafka for the Lagom Message Broker API for
communication between services
Aggregates are stored in-memory while the journal is
persisted to Cassandra.
In this example, we have a three node Kubernetes
cluster, with two bounded contexts spread across those
nodes. This offers resilience and scale.
Deploying to Minikube
Instructions are available here to help deploy Reactive
Stock Trader (along with Cassandra and Kafka) to
Minikube:
https://github.com/RedElastic/reactive-stock-trader/tree/
master/deploy/instructions
Conclusion
— CQRS separates writes and reads for reliability and
performance
— Event sourcing eliminates mutability concerns of
relational databases while preserving their query
capabilities
— Operationally Lagom is cloud-native and ready to
deploy to AWS, Azure, GCP, etc, via Kubernetes
What about serverless?
— For the foreseeable future we
need to understand how our
software interacts with the
runtime environment
— A serverless component may
be part of a reactive system,
but is not a replacement for a
reactive system
— Reactive systems enable
portability across cloud
vendors, whereas many
serverless offerings lock us in
Reactive in Practice
For a complete look at this
material, visit IBM Developer and
check out Reactive in Practice, a
12 part series (parts 8-12 to be
published mid-September).
https://developer.ibm.com/series/
reactive-in-practice/
Thanks to Dana Harrington,
Lightbend, and IBM.

Full Stack Reactive In Practice

  • 2.
    Full Stack Reactivein Practice Presenting a real-world CQRS (command query responsibility segregation) and ES (event sourcing) architecture. Kevin Webber Director of Engineering @ Reading Plus w: kevinwebber.ca m: medium.com/@kvnwbbr t: @kvnwbbr
  • 3.
    — Reading Plusis an adaptive platform that helps K-12 students become successful readers and lifelong learners — We will be applying reactive techniques and technologies to a mission with purpose — Find us at https://readingplus.com and stay tuned for upcoming engineering opportunities
  • 4.
    Why Reactive? — React,Vue, and other front-end frameworks are enabling innovative user experiences
  • 5.
    Why Reactive? — React,Vue, and other front-end frameworks are enabling innovative user experiences — Meanwhile, relational databases continue to dominate the server side
  • 6.
    Why Reactive? — React,Vue, and other front-end frameworks are enabling innovative user experiences — Meanwhile, relational databases continue to dominate the server side — Reactive principles and frameworks help create an end-to-end event-driven channel, all the way up and all the way down
  • 7.
    Traditional architecture — Unboundedin complexity and side effects — Putting React or Vue on top won't help much — Batch-mode, high-latency, high-failure rates — Has an impact — Customer happiness? — Developer joy? — Visit TheDailyWTF :)
  • 8.
    What we willcover All references and examples will be in Java. 1. Designing reactive systems → Event Storming, DDD 2. Implementing reactive systems → Lagom, Play, Akka 3. Deploying reactive systems → sbt, Kubernetes
  • 9.
    Part 1. DesigningReactive Systems — Raw ingredients — Commands & events — Inception — Event storming & DDD — Fidelity — Event modeling — Handoff — User stories
  • 10.
    Systems have beenevent driven for centuries!
  • 11.
    Events are — Interestingthings, that have — happened in the past.
  • 12.
    Event sourcing — Commandsare applied against an entity, the entity can accept or reject a command — If accepted, an entity will generate an event and then apply that event in order to change state
  • 13.
    Defining aggregate boundaries —Structures implementation around related items — The yellow sticky in the middle represents state
  • 14.
    Defining bounded contexts —Group together related business concepts — Contain domain complexity — A single person can master an aggregate — A single team can master a bounded context
  • 15.
    How do wemodel a bounded context? — Arrange events in linear order by time — Events cluster into closely related groups (aggregates) — Aggregates cluster into closely related groups (bounded contexts) — Event Storming helps discover the problem space — Event Modeling creates a blueprint for a solution
  • 16.
  • 17.
    Essential learning resources —Book: Event Storming by Alberto Brandolini — Book: Domain-Driven Design Distilled by Vaughn Vernon — Website: Event Modeling by Adam Dymitruk
  • 18.
    Part 2. Implementation —Tools — Lagom, Play, and Akka — Cassandra and Kafka — Pa!erns — Event sourcing, CQRS — Architecture — Interface, BFF, services — Integration — Point to point, streaming, pub/sub
  • 19.
    Reactive Stock Trader —Reference application for Reactive in Practice — Vue, Lagom, Play, Akka, Cassandra, Kafka, Kubernetes — Bounded contexts — Portfolio — Broker — Wire transfers — https://github.com/RedElastic/reactive-stock-trader
  • 21.
    Placing a wiretransfer We'll cover the command channel first. 1. Create command 2. Send over REST 3. Direct command to entity 4. Create event 5. Change state 6. Publish event
  • 23.
    Submit transfer request(Vue.js) onSubmit() { this.submitted = true; submitTransfer(this.form) .then(() => { this.submitted = false; this.onReset(); }); },
  • 24.
    API gateway — Backendfor Frontend (BFF) is a pattern that involves creating a 1:1 relationship between backends and UIs — The BFF then handles all calls to various underlying microservices (Portfolio, Broker, Wires, etc) — Also performs authentication, authorization, etc
  • 25.
    Play (command sideAPI) @Override public ServiceCall<Transfer, TransferId> transferFunds() { TransferId transferId = TransferId.newId(); return transfer -> transferRepository .get(transferId) // 1 (get reference to entity) .ask(TransferCommand.TransferFunds.builder() .source(transfer.getSourceAccount()) .destination(transfer.getDestinationAccount()) .amount(transfer.getFunds()) .build() ) // 2 (ask pattern) .thenApply(done -> transferId); }
  • 26.
    BFF and ServiceIntegration — Play routes all commands and queries to Lagom microservices — Lagom is a framework built on top of Akka for developing reactive microservices — Based on the principles of CQRS (Command Query Responsibility Segregation)
  • 27.
    Lagom (aggregates /entities) — Based around persistent entities (aka, aggregates) — Stored in memory for fast access — Can be recovered via Cassandra as the event log public class TransferEntity extends PersistentEntity<TransferCommand, TransferEvent, Optional<TransferState>> { // ... }
  • 28.
    Lagom (event sourcing) —No mutable data, only immutable state — State is only changed on successful event application — Events are journaled — Entity can always recover in- memory by replaying events from the journal — Entities are not queried directly, read-side queries are backed by views
  • 29.
    State machines Let's saywe're modeling an ATM machine... — This ATM machine has three different states — Each state has a different behaviour for the same input https://www.uml-diagrams.org/ state-machine-diagrams.html
  • 30.
    Lagom (event sourcing) privateBehavior empty() { BehaviorBuilder builder = newBehaviorBuilder(Optional.empty()); builder.setCommandHandler(TransferCommand.TransferFunds.class, (cmd, ctx) -> { // 1 (handler is invoked from ask pattern) TransferDetails transferDetails = TransferDetails.builder() .source(cmd.getSource()) .destination(cmd.getDestination()) .amount(cmd.getAmount()) .build(); // 2 (build up details of xfer) ObjectMapper mapper = new ObjectMapper(); TransferUpdated tc = buildTransferUpdated( transferDetails, "Transfer Initiated"); publishedTopic.publish( mapper.valueToTree(tc).toString()); // 3 (publish to stream) return ctx.thenPersist( new TransferEvent .TransferInitiated( getTransferId(), transferDetails), evt -> ctx.reply(Done.getInstance()) ); // 4 (persist event & return ID) }); builder.setEventHandlerChangingBehavior( TransferEvent.TransferInitiated.class, this::fundsRequested); // 5 (change state) return builder.build(); }
  • 35.
    Views — Read sideprocessors build a precomputed table — Queries are only against precomputed tables — Use this to populate data on initial page load
  • 36.
    Creating a portfolio 1.When each portfolio is created, we create a portfolio entity and then persist the create event to the journal 2. A read side processor subscribes to journal updates 3. With each update, the read side processor updates a projected view Let's cover #3...
  • 37.
    Lagom ReadSide processor Updatea precomputed query table (Cassandra) on every new event we subscribe to. @Override public ReadSideHandler<PortfolioEvent> buildHandler() { return readSide.<PortfolioEvent>builder("portfolio_offset") // 1 .setGlobalPrepare(this::prepareCreateTables) // 2 .setPrepare(tag -> prepareWritePortfolios()) // 3 .setEventHandler(Opened.class, this::processPortfolioChanged) // 4 .build(); } private CompletionStage<Done> prepareWritePortfolios() { return session .prepare("INSERT INTO portfolio_summary (portfolioId, name) VALUES (?, ?)") .thenApply(ps -> { this.writePortfolios = ps; return Done.getInstance(); }); }
  • 38.
    Queries are thenexecuted against the projected view for a vastly lower latency user experience. @Override public ServiceCall<NotUsed, PSequence<PortfolioSummary>> getAllPortfolios() { return request -> { CompletionStage<PSequence<PortfolioSummary>> result = db.selectAll("SELECT portfolioId, name FROM portfolio_summary;") .thenApply(rows -> { List<PortfolioSummary> summary = rows.stream().map(row -> PortfolioSummary.builder() .portfolioId(new PortfolioId( row.getString("portfolioId"))) .name(row.getString("name")) .build()) .collect(Collectors.toList()); return TreePVector.from(summary); }); return result; }; }
  • 39.
    Streaming 1. Render initialpage with precomputed view over HTTP/ REST 2. Switch to unidirectional streaming for updates (events over WS) 3. Commands over REST will still cause full page refreshes (can change unidirectional stream to BiDi stream in future) Instead, let's use the Lagom PubSub API to push updates to the UI in real-time...
  • 40.
    Streaming (Lagom) This codeexposes a Reactive Streams Source via Lagom, which Play can then "attach" to. @Override public ServiceCall<NotUsed, Source<String, ?>> transferStream() { return request -> { // subscribe to events on a specific topic ("transfer") final PubSubRef<String> topic = pubSub.refFor( TopicId.of(String.class, "transfer")); // return the Source as a future (standard async Java 8) return CompletableFuture.completedFuture(topic.subscriber()); }; }
  • 41.
    Streaming (architecture) PubSub worksby broadcasting events to subscribers: — Publisher is TransferEntity — Subscriber is WireTransferServiceImpl — This will create a streaming Source
  • 42.
    Create a streamingconnection all the way down.
  • 43.
    WireTransferController public WebSocket ws(){ return WebSocket.Text.acceptOrResult(req -> { return wireTransferService .transferStream() .invoke() .thenApply(source -> { return F.Either.Right( Flow.fromSinkAndSource( Sink.ignore(), source ) ); }); }); }
  • 44.
    WebSockets (Vue.js) connect() { this.socket= new WebSocket( "ws://localhost:9000/api/transfer/stream"); this.socket.onopen = () => { this.socket.onmessage = (e) => { let event = JSON.parse(e.data); var index = -1; // 1. determine if we're updating a row (initiated) // or adding a new row (completed) for (var i = 0; i < this.transfers.length; i++) { if (this.transfers[i].id === event.id) { index = i; break; } } if (index === -1) { // unshift is similar to push, but prepends this.transfers.unshift({ // ... 3. create object with id, status, etc }); } else { let t = { // ... 4. create object with id, status, etc }; this.transfers.splice(index, 1, t); this.updateCashOnHand(); } }; }; }
  • 45.
    Part 3. Deployment —Packaging — sbt, service boundaries — Testing — Deploying to Minikube — Handling dependencies in k8s such as Cassandra, Kafka — Deployment — Production deployments
  • 46.
  • 47.
    Reactive Stock Traderneeds... — Formation of a cluster so Lagom entities can stay available in memory — Connection to Cassandra and Kafka (in a "high- availability" clustered configuration) — Cassandra for the backing journal — Kafka for the Lagom Message Broker API for communication between services
  • 48.
    Aggregates are storedin-memory while the journal is persisted to Cassandra.
  • 49.
    In this example,we have a three node Kubernetes cluster, with two bounded contexts spread across those nodes. This offers resilience and scale.
  • 50.
    Deploying to Minikube Instructionsare available here to help deploy Reactive Stock Trader (along with Cassandra and Kafka) to Minikube: https://github.com/RedElastic/reactive-stock-trader/tree/ master/deploy/instructions
  • 51.
    Conclusion — CQRS separateswrites and reads for reliability and performance — Event sourcing eliminates mutability concerns of relational databases while preserving their query capabilities — Operationally Lagom is cloud-native and ready to deploy to AWS, Azure, GCP, etc, via Kubernetes
  • 52.
    What about serverless? —For the foreseeable future we need to understand how our software interacts with the runtime environment — A serverless component may be part of a reactive system, but is not a replacement for a reactive system — Reactive systems enable portability across cloud vendors, whereas many serverless offerings lock us in
  • 53.
    Reactive in Practice Fora complete look at this material, visit IBM Developer and check out Reactive in Practice, a 12 part series (parts 8-12 to be published mid-September). https://developer.ibm.com/series/ reactive-in-practice/ Thanks to Dana Harrington, Lightbend, and IBM.