Download - Building Microservices with Scala, functional domain models and Spring Boot - Chris Richardson
@crichardson
Building microservices with Scala, functional domain models and Spring Boot
Chris Richardson
Author of POJOs in ActionFounder of the original CloudFoundry.com
@[email protected]://plainoldobjects.com
@crichardson
Presentation goal
Share my experiences with building an application using Scala, functional domain
models, microservices, event sourcing, CQRS, and Spring Boot
@crichardson
About Chris
@crichardson
About Chris
Founder of a buzzword compliant (stealthy, social, mobile, big data, machine learning, ...) startup
Consultant helping organizations improve how they architect and deploy applications using cloud, micro services, polyglot applications, NoSQL, ...
@crichardson
Agenda
Why build event-driven microservices?
Overview of event sourcing
Designing microservices with event sourcing
Implementing queries in an event sourced application
Building and deploying microservices
@crichardson
Let’s imagine that you are building a banking app...
@crichardson
Domain model
Account
balance
open(initialBalance)debit(amount)credit(amount)
MoneyTransfer
fromAccountIdtoAccountIdamount
@crichardson
Tomcat
Traditional application architecture
Browser/Client
WAR/EAR
RDBMS
Customers
Accounts
Transfers
Banking Banking UI
developtest
deploy
Simple to
Load balancer
scale
Spring MVC
SpringHibernate
...
HTMLREST/JSON
@crichardson
Problem #1: monolithic architecture
Intimidates developers
Obstacle to frequent deployments
Overloads your IDE and container
Obstacle to scaling development
Requires long-term commitment to a technology stack
@crichardson
Solution #1: use a microservice architecture
Banking UI
Account Management Service
Transaction Management Service
Account Database
Transaction Database
Standaloneservices
@crichardson
Problem #2: relational databases
Scalability
Distribution
Schema updates
O/R impedance mismatch
Handling semi-structured data
@crichardson
Solution #2: use NoSQL databases
Avoids the limitations of RDBMS
For example,
text search ⇒ Solr/Cloud Search
social (graph) data ⇒ Neo4J
highly distributed/available database ⇒ Cassandra
...
@crichardson
Different modules use different databases
IEEE Software Sept/October 2010 - Debasish Ghosh / Twitter @debasishg
@crichardson
But now we have problems with data consistency!
@crichardson
Problem #3: Microservices = distributed data management
Each microservice has it’s own database
Some data is replicated and must be kept in sync
Business transactions must update data owned by multiple services
Tricky to implement reliably without 2PC
@crichardson
Problem #4: NoSQL = ACID-free, denormalized databases
Limited transactions, i.e. no ACID transactions
Tricky to implement business transactions that update multiple rows, e.g. http://bit.ly/mongo2pc
Limited querying capabilities
Requires denormalized/materialized views that must be synchronized
Multiple datastores (e.g. DynamoDB + Cloud Search ) that need to be kept in sync
@crichardson
Solution to #3/#4: Event-based architecture to the rescue
Microservices publish events when state changes
Microservices subscribe to events
Synchronize replicated data
Maintains eventual consistency across multiple aggregates (in multiple datastores)
@crichardson
Eventually consistent money transfer
Message Bus
MoneyTransferService AccountService
transferMoney()
Publishes:Subscribes to:
Subscribes to:
publishes:MoneyTransferCreatedEvent
AccountDebitedEvent
DebitRecordedEvent
AccountCreditedEventMoneyTransferCreatedEvent
DebitRecordedEvent
AccountDebitedEventAccountCreditedEvent
@crichardson
To maintain consistency a service must
atomically publish an event whenever
a domain object changes
@crichardson
How to generate events reliably?
Database triggers, Hibernate event listener, ...
Reliable BUT
Not with NoSQL
Disconnected from the business level event
Limited applicability
Ad hoc event publishing code mixed into business logic
Messy code, poor separation of concerns
Unreliable, e.g. too easy to forget to publish an event
@crichardson
How to atomically update datastore and publish event(s)
Use 2PC
Database and message broker must support 2PC
2PC is best avoided
Use datastore as a message queue
1. Update database: new entity state & event to publish
2. Publish event & mark event as published
Eventually consistent mechanism
See BASE: An Acid Alternative, http://bit.ly/ebaybase
• Difficult to implement when using a NoSQL database :-(
@crichardson
Agenda
Why build event-driven microservices?
Overview of event sourcing
Designing microservices with event sourcing
Implementing queries in an event sourced application
Building and deploying microservices
@crichardson
Event sourcingFor each aggregate:
Identify (state-changing) domain events
Define Event classes
For example,
Account: AccountOpenedEvent, AccountDebitedEvent, AccountCreditedEvent
ShoppingCart: ItemAddedEvent, ItemRemovedEvent, OrderPlacedEvent
@crichardson
Persists events NOT current state
Account
balance
open(initial)debit(amount)credit(amount)
AccountOpened
Event table
AccountCredited
AccountDebited
101 450
Account tableX101
101
101
901
902
903
500
250
300
@crichardson
Replay events to recreate state
Account
balance
AccountOpenedEvent(balance)AccountDebitedEvent(amount)AccountCreditedEvent(amount)
Events
@crichardson
Aggregate traits
Map Command to Events
Apply event returning updated Aggregate
@crichardson
Account - command processing
Prevent overdraft
@crichardson
Account - applying eventsImmutable
@crichardson
Request handling in an event-sourced application
HTTPHandler
EventStore
pastEvents = findEvents(entityId)
Account
new()
applyEvents(pastEvents)
newEvents = processCmd(SomeCmd)
saveEvents(newEvents)
Microservice A
@crichardson
Event Store publishes events - consumed by other services
EventStore
EventSubscriber
subscribe(EventTypes)
publish(event)
publish(event)
Aggregate
NoSQLmaterialized
view
update()
update()
Microservice B
@crichardson
Persisting events
Ideally use a cross platform format
Use weak serialization:
enables event evolution, eg. add memo field to transfer
missing field ⇒ provide default value
unknown field ⇒ ignore
JSON is a good choice
@crichardson
Optimizing using snapshots
Most aggregates have relatively few events
BUT consider a 10-year old Account ⇒ many transactions
Therefore, use snapshots:
Periodically save snapshot of aggregate state
Typically serialize memento of the aggregate
Load latest snapshot + subsequent events
@crichardson
Event Store APItrait EventStore {
def save[T <: Aggregate[T]](entity: T, events: Seq[Event], assignedId : Option[EntityId] = None): Future[EntityWithIdAndVersion[T]]
def update[T <: Aggregate[T]](entityIdAndVersion : EntityIdAndVersion, entity: T, events: Seq[Event]): Future[EntityWithIdAndVersion[T]]
def find[T <: Aggregate[T] : ClassTag](entityId: EntityId) : Future[EntityWithIdAndVersion[T]]
def findOptional[T <: Aggregate[T] : ClassTag](entityId: EntityId) Future[Option[EntityWithIdAndVersion[T]]]
def subscribe(subscriptionId: SubscriptionId): Future[AcknowledgableEventStream]}
@crichardson
Business benefits of event sourcing
Built-in, reliable audit log
Enables temporal queries
Publishes events needed by big data/predictive analytics etc.
Preserved history ⇒ More easily implement future requirements
@crichardson
Technical benefits of event sourcing
Solves data consistency issues in a Microservice/NoSQL-based architecture:
Atomically save and publish events
Event subscribers update other aggregates ensuring eventual consistency
Event subscribers update materialized views in SQL and NoSQL databases (more on that later)
Eliminates O/R mapping problem
@crichardson
Drawbacks of event sourcing
Weird and unfamiliar
Events = a historical record of your bad design decisions
Handling duplicate events can be tricky
Application must handle eventually consistent data
Event store only directly supports PK-based lookup (more on that later)
@crichardson
Example of an eventual consistency problem
Scenario:
1.Create the user
2.Create shopping cart
3.Update the user with the shopping cart’s id
The user temporarily does not have a shopping cart id!
Client might need to retry their request at a later point
Server should return status code 418??
@crichardson
Handling duplicate messages
Idempotent operations
e.g. add item to shopping cart
Duplicate detection:
e.g. track most recently seen event and discard earlier ones
@crichardson
Agenda
Why build event-driven microservices?
Overview of event sourcing
Designing microservices with event sourcing
Implementing queries in an event sourced application
Building and deploying microservices
@crichardson
The anatomy of a microservice
Event Store
HTTP Request
HTTP Adapter
Aggregate
Event Adapter
Cmd
Cmd
EventsEvents
Xyz Adapter
Xyz Request
microservice
@crichardson
Asynchronous Spring MVC controller
@crichardson
MoneyTransferService
DSL concisely specifies:1.Creates MoneyTransfer aggregate2.Processes command3.Applies events4.Persists events
@crichardson
MoneyTransfer Aggregate
@crichardson
Handling events published by Accounts
1.Load MoneyTransfer aggregate2.Processes command3.Applies events4.Persists events
@crichardson
Agenda
Why build event-driven microservices?
Overview of event sourcing
Designing microservices with event sourcing
Implementing queries in an event sourced application
Building and deploying microservices
@crichardson
Let’s imagine that you want to display an account and it’s recent transactions...
@crichardson
Displaying balance + recent credits and debits
We need to do a “join: between the Account and the corresponding MoneyTransfers
(Assuming Debit/Credit events don’t include other account, ...)
BUTEvent Store = primary key lookup of individual aggregates, ...
⇒Use Command Query Responsibility Separation
Define separate “materialized” query-side views that implement those queries
@crichardson
Query-side microservices
Event Store
Updater - microservice
View UpdaterService
EventsReader - microservice
HTTP GET Request
View Query Service
ViewStore
e.g. MongoDB
Neo4JCloudSearch
update query
@crichardson
Persisting account balance and recent transactions in MongoDB
{ id: "298993498", balance: 100000, transfers : [
{"transferId" : "4552840948484", "fromAccountId" : 298993498, "toAccountId" : 3483948934, "amount" : 5000}, ...
], changes: [ {"changeId" : "93843948934", "transferId" : "4552840948484", "transactionType" : "AccountDebited", "amount" : 5000}, ... ] }
Denormalized = efficient lookup
Transfers that update the account
The sequence of debits and credits
@crichardson
Updating MongoDB using Spring Data
class AccountInfoUpdateService (mongoTemplate : MongoTemplate, ...) extends CompoundEventHandler {
@EventHandler def recordDebit(de: DispatchedEvent[AccountDebitedEvent]) = { ... val ci = AccountChangeInfo(...)
mongoTemplate.updateMulti( new Query(where("id").is(de.entityId.id).and("version").lt(changeId)), new Update(). dec("balance", amount). push("changes", ci). set("version", changeId), classOf[AccountInfo]) }
@EventHandler def recordTransfer(de: DispatchedEvent[MoneyTransferCreatedEvent]) = ...
}
insert/In-place update
duplicate event detection
updates to and from accounts
@crichardson
Retrieving account info from MongoDB using Spring Data
class AccountInfoQueryService(accountInfoRepository : AccountInfoRepository) {
def findByAccountId(accountId : EntityId) : AccountInfo = accountInfoRepository.findOne(accountId.id)
}
case class AccountInfo(id : String, balance : Long, transactions : List[AccountTransactionInfo], changes : List[ChangeInfo], version : String)
case class AccountTransactionInfo(changeId : String, transactionId : String,
transactionType : String, amount : Long, balanceDelta : Long)
trait AccountInfoRepository extends MongoRepository[AccountInfo, String]
Implementation generated by Spring Data
@crichardson
Other kinds of viewsAWS Cloud Search
Text search as-a-Service
View updater batches aggregates to index
View query does text search
AWS DynamoDB
NoSQL as-a-Service
On-demand scalable - specify desired read/write capacity
Document and key-value data models
Useful for denormalized, UI oriented views
@crichardson
Dealing with eventually consistent views
Scenario:
Client creates/updates aggregate
Client requests view of aggregate
But the view might not yet have been updated
Solution:
Create/Update response contains aggregate version
Query request contains desired version
Alternatively:
“Fake it” in the UI until the view is updated
@crichardson
Agenda
Why build event-driven microservices?
Overview of event sourcing
Designing microservices with event sourcing
Implementing queries in an event sourced application
Building and deploying microservices
@crichardson
Building microservices with Spring Boot
Makes it easy to create stand-alone, production ready Spring Applications
Automatically configures Spring using Convention over Configuration
Externalizes configuration
Generates standalone executable JARs with embedded web server
@crichardson
Spring Boot simplifies configuration
Spring Container
Application components
Fully configured application
ConfigurationMetadata
•Typesafe JavaConfig•Annotations•Legacy XML
Default Configuration
Metadata
Spring BootYou write less
of this
Inferred from CLASSPATH
@crichardson
Tiny Spring configuration for Account microservice
@Configuration@EnableAutoConfiguration@Import(classOf[JdbcEventStoreConfiguration]))@ComponentScanclass AccountConfiguration {
@Bean def accountService(eventStore : EventStore) = new AccountService(eventStore)
@Bean def accountEventHandlers(eventStore : EventStore) = EventHandlerRegistrar.makeFromCompoundEventHandler( eventStore, "accountEventHandlers", new TransferWorkflowAccountHandlers(eventStore)) @Bean @Primary def scalaObjectMapper() = ScalaObjectMapper
}
Service
Event handlers
Scan for controllers
Customize JSON serialization
@crichardson
The Main program
object BankingMain extends App {
SpringApplication.run(classOf[AccountConfiguration], args :_ *)
}
@crichardson
Building with Gradle
buildscript { repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:1.0.0.RELEASE") }}
apply plugin: 'scala'apply plugin: 'spring-boot'
...
@crichardson
Running the microservice
$ java -jar build/libs/banking-main-1.0-SNAPSHOT.jar --server.port=8081...11:38:04.633 INFO n.c.e.e.bank.web.main.BankingMain$ - Started BankingMain. in 8.811 seconds (JVM running for 9.884)
$ curl localhost:8081/health{"status":"UP", "mongo":{"status":"UP","version":"2.4.10"}, "db":{"status":"UP","database":"H2","hello":1}}
Built in health checks
Command line arg processing
@crichardson
NodeJS-based API gateway
Motivations
Small footprint
Efficient, event-driven I/O
Availability of modules: express, request, googleapis, ...
Routes requests to the appropriate backend service based on URL prefix
Handles Google/OAuth-based authentication
@crichardson
Jenkins-based deployment pipeline
Build & Testmicro-service
Build & TestDockerimage
Deploy Docker image
to registry
One pipeline per micro-service
@crichardson
Production environment
EC2 instance running Docker
Deployment tool:
1.Compares running containers with what’s been built by Jenkins
2.Pulls latest images from Docker registry
3.Stops old versions
4.Launches new versions
@crichardson
Summary
Event Sourcing solves key data consistency issues with:
Microservices
Partitioned/NoSQL databases
Spring and Scala play nicely together
Spring Boot makes it very easily to build production ready microservices
NodeJS is useful for building network-centric services
@crichardson
Questions?
@crichardson [email protected]
http://plainoldobjects.com http://microservices.io