SlideShare a Scribd company logo
CQRS and Event Sourcing
with MongoDB and PHP
About me
Davide Bellettini
● Developer at Onebip
● TDD addicted
@SbiellONE — about.bellettini.me
What is this talk about
A little bit of context
About Onebip
Mobile payment platform.
Start-up born in 2005,
acquired by Neomobile
group in 2011.
Onebip today:
- 70 countries
- 200+ carriers
- 5 billions potential users
LAMP stack
It all started with a Monolith
self-contained services communicating via REST
To a distributed system
First class modern NoSQL distributed dbs
Modern services
But the Monolith is still there
The problem
A reporting horror story
We need three new reports!
― Manager
Sure, no problem!
Deal with the legacy SQL schema
Deal with MongoDB
A little bit of queries here,
a little bit of map-reduce there
1 month later...
Reports are finally ready!
until...
Your queries are killing production!
― SysAdmin
Still not enough!
Heavy query optimization,
adding indexes
Let’s reuse data from other reports
(don’t do that)
DB is ok, reports delivered.
but then...
Houston, we have a problem. Reports are
not consistent (with other reports)
― Business guy
Mistakes
were
made
Lessons
learned
It’s hard to compare different data in a distributed
system splitted across multiple domains
#1
Avoid multiple sources of truth
Same words, different concepts across domains
#2
Ubiquitous language
Changing a report shouldn’t have side effects
#3
Fault tolerance to change
Most common solutions
#1
ETL + Map-Reduce
#2
Data Warehouse + Consultants
#3
Mad science (Yeppa!)
What we wanted
No downtime in production
Consistent across domains
Must have
A system elastic enough to extract any metric
Real time data
Nice to have
In DDD we found the light
CQRS and Event Sourcing
Command-query
responsibility segregation
(CQRS)
Commands
Anything that happens in one of your domains
is triggered by a command and generates one
or more events.
Order received -> payment sent -> Items queued
-> Confirmation email sent
Query
Generate read models from events depending
how data need to be actually used (by users
and other application internals)
Event Sourcing
The fundamental idea of Event Sourcing is that of ensuring
every change to the state of an application is captured in an
event object, and that these event objects are themselves
stored in the sequence they were applied.
― Martin Fowler
Starting from the beginning of time, you are
literally unrolling history to reach state in a
given time
Unrolling a stream of events
Idea #1
Every change to the state of your application is
captured in event object.
“UserLoggedIn”, “PaymentSent”, “UserLanded”
Idea #2
Events are stored in
the sequence they
were applied inside
an event store
Idea #3
Everything is an event. No more state.
Idea #4
One way to store data/events but potentially
infinite ways to read them.
A practical example
Tech ops, business control, monitoring,
accounting they all are interested in reading
data from different views.
Healthy NoSQL
You start with this
{
"_id": ObjectId("123"),
"username": "Flash",
"city": …,
"phone": …,
"email": …,
}
The more successful your company
is, the more people
…
The more people, the more views
With documental dbs it's magically easy to add new
fields to your collections.
Soon you might end up with
{
"_id": ObjectId("123"),
"username": "Flash",
"city": …,
"phone": …,
"email": …,
"created_at": …,
"updated_at": …,
"ever_tried_to_purchase_something": …,
"canceled_at": …,
"acquisition_channel": …,
"terminated_at": …,
"latest_purchase_date": …,
…
}
A bomb waiting to detonate
It’s impossible to keep adding state changes to your
documents and then expect to be able to extract them with
a single query.
Exploring
Tools
Event Store
● Engineered for event sourcing
● Supports projections
● By the father of CQRS (Greg Young)
● Great performances
http://geteventstore.com/
The bad
Based on Mono, still too unstable.
LevelWHEN
An event store built with Node.js and LevelDB
● Faster than light
● Completely custom, no tools to handle
aggregates
https://github.com/gabrielelana/levelWHEN
The known path
● PHP (any other language
would just do fine)
● MongoDB 2.2.x
Why MongoDB
Events are not relational
Scales well
Awesome aggregation framework
Hands on
Storing Events
Service |
 |
 [event payload] |
 |
Service --- Queue System <------------> API -> MongoDB
/ |
/ [event payload] |
/ |
Service |
The write architecture
Queues
Recruiter - https://github.com/gabrielelana/recruiter
MongoDB replica set
A MongoDB replica set with two logical dbs:
1. Event store where we would store events
2. Reporting DB where we would store
aggregates and final reports
Anatomy of an event 1/2
{
'_id' : '3318c11e-fe60-4c80-a2b2-7add681492d9',
'type': 'an-event-type',
'data': {
'meta' : {
…
},
'payload' : {
…
}
}
}
Anatomy of an event 2/2
'meta' : {
'creation_date': ISODate("2014-21-11T00:00:01Z"),
'saved_date': ISODate("2014-21-11T00:00:02Z"),
'source': 'some-bounded-context',
'correlation_id': 'a-correlation-id'
},
'payload' : {
'user_id': '1234',
'animal': 'unicorn',
'colour': 'pink',
'purchase_date': ISODate("2014-21-11T00:00:00Z"),
'price': '20/fantaeuros'
}
Don’t trust the network: Idempotence
{
'_id' : '3318c11e-fe60-4c80-a2b2-7add681492d9',
…
}
The _id field is actually defined client side and
ensures idempotence if an event is received
two times
Indexes
● Events collection is huge (~100*N documents)
● Use indexes wisely as they are necessary yet
expensive
● With suggested event structure:
{‘data.meta.created_at’: 1, type:1}
Benchmarking
How many events/second can you store?
Our machines were able to store roughly 150 events/sec.
This number can be greatly increased with dedicated IOPS,
more aggressive inserting policies, etc...
Final tips
● Use SSD on your storage machines
● Pay attention to write concerns (w=majority)
● Test your replica set fault tolerance
From events
to meaningful metrics
Sequential Projector -> Event Mapper -> Projection -> Aggregation
The event processing pipeline
A real life problem
What is the conversion rate of our registered
users?
#1 The registration event
{
'_id' : '3318c11e-fe60-4c80-a2b2-7add681492d9',
'type': 'user-registered',
'data': {
'meta' : {
'save_date': ISODate("2014-21-11T00:00:09Z"),
'created_at': ISODate("2014-21-11T00:00:01Z"),
'source': 'core-domain',
'correlation_id': 'user-123456'
},
'payload' : {
'user_id': 123,
'username': 'flash',
'email': 'a-dummy-email@gmail.com',
'country': 'IT'
}
}
#2 The purchase event
{
'_id' : '3318c11e-fe60-4c80-a2b2-7add681492d9',
'type': 'user-purchased',
'data': {
'meta' : {
'save_date': ISODate("2014-21-11T00:10:09Z"),
'created_at': ISODate("2014-21-11T00:10:01Z"),
'source': 'payment-gateway',
'correlation_id': 'user-123456'
},
'payload' : {
'user_id': 123,
'email': 'a-dummy-email@gmail.com',
'amount': 20,
'value': EUR,
'payment': 'credit_card',
'item': 'fluffy cat'
}
}
Sequential projector 1/2
[]->[x]->[]->[x]->[]->[]->[]->[]
|--------------| |------------|
|
|
|
|
---> Projector
Divides the stream of events into batches, filters events by
type and pass those of interest to the mapper
Sequential projector 2/2
● It’s a good idea to select fixed sizes batches to avoid
memory problems when you load your Cursor in memory
● Could be a long-running process selecting events as they
arrive in realtime
Event mapper 1/3
Translates event fields to the Read Model domain
Takes an event as input, applies a bunch of logic and will
return a list of Read Model fields.
Event mapper 2/3
Input event:
user-registered
Output:
$output = [
'user_id' => 123, // simply copied
'user_name' => 'flash', // simply copied
'email' => 'a-dummy-email@gmail.com', // simply copied
'registered_at' => "2014-21-11T00:00:01Z" // From the data.meta.created_at event field
];
Event mapper 3/3
Input event:
user-purchased
Output:
$output = [
'user_id' => 123, // simply copied
'email' => 'a-dummy-email@gmail.com', // simply copied
'purchased_at': "2014-21-11T00:10:01Z" // From the data.meta.created_at event field
];
Projection
Essentially it is your read model.
The data that the business is interested in.
The Projection after event #1
db.users_conversion_rate_projection.findOne()
{
'user_id': 123,
'user_name': 'flash',
'email': 'a-dummy-email@gmail.com',
'registered_at': "2014-21-11T00:00:01Z"
}
The Projection after event #2
{
'user_id': 123,
'user_name': 'flash',
'email': 'a-dummy-email@gmail.com',
'registered_at': "2014-21-11T00:00:01Z"
'purchased_at': "2014-21-11" // Added this field and rewrote others
}
The Projection collection{
'user_id': 123,
'user_name': 'flash',
'email': 'a-dummy-email@gmail.com',
'registered_at': "2014-21-11",
'purchased_at': "2014-21-11" // Added this field and rewrote others
}
{
'user_id': 456,
'user_name': 'batman',
'email': 'a-dummy-email@gmail.com',
'registered_at': "2014-21-11",
'purchased_at': "2014-21-11" // Added this field and rewrote others
}
{
'user_id': 789,
'user_name': 'superman',
'email': 'a-dummy-email@gmail.com',
'registered_at': "2014-21-12",
'purchased_at': "2014-21-12" // Added this field and rewrote others
}
The Projection - A few thoughts
Note that we didn't copy from events to projection
all the available fields. Just relevant ones.
From these two events we could have
generated infinite read models such as
● List all purchased products and related amounts for the
company buyers
● Map all sales and revenues for our accounting dept
● List transactions for the financial department
One way to write,
infinite ways to read!
The aggregation (1) - Total registered users
var registered = db.users_conversion_rate_projection.aggregate([
{
$match: {
"registered_at": { $gte: ISODate("2015-11-21"), $lte: ISODate("2015-11-22") }
}
},
{
$group: {
_id: { },
count: { $sum:1 }
}
}
]);
The aggregation (2) - User with a purchase
var purchased = db.users_conversion_rate_projection.aggregate([
{
$match: {
"registered_at": { $gte: ISODate("2015-11-21"), $lte: ISODate("2015-11-22") },
"purchased_at": { $exists: true }
}
},
{
$group: {
_id: { },
count: { $sum:1 }
}
}
]);
The aggregation (3) - Automate all the things
● You can easily create the aggregation framework statement
by composition abstracting the concept of Column.
● This way you can dynamically aggregate your projections
on (for example) an API requests.
● If your Projector is a long running process, your projections
will be updated to the second and you automagically get
realtime data.
Another events usage:
Business & Tech Monitoring
Beware of the beast!
No Silver Bullet
Events are expensive
They require a lot of TIME to be parsed
Events are expensive
You will end up with this billion size collection
(and counting)
Fixing wrong events is painful
Events are complex
Moving around events is
horribly painful
Actually it will make your life incredibly
difficult with hidden bugs and leaking
documentation.
Mongo won’t help you
Improvements
● Upgrade from MongoDB 2.2.x to 3.0.x
● Switch to WiredTiger storage engine to save
space
Credits
Based on a talk by Jacopo Nardiello
● Slides: http://bit.ly/es-nardiello-2014
● Video: https://vimeo.com/113370688
Q&A
@SbiellONE — about.bellettini.me
Thank you!

More Related Content

What's hot

Writing and using Hamcrest Matchers
Writing and using Hamcrest MatchersWriting and using Hamcrest Matchers
Writing and using Hamcrest Matchers
Shai Yallin
 
JSON-LD and MongoDB
JSON-LD and MongoDBJSON-LD and MongoDB
JSON-LD and MongoDB
Gregg Kellogg
 
Spring security
Spring securitySpring security
Spring security
Saurabh Sharma
 
Indexing with MongoDB
Indexing with MongoDBIndexing with MongoDB
Indexing with MongoDB
MongoDB
 
Introduction to Activiti BPM
Introduction to Activiti BPMIntroduction to Activiti BPM
Introduction to Activiti BPM
Alfresco Software
 
Railway Oriented Programming
Railway Oriented ProgrammingRailway Oriented Programming
Railway Oriented Programming
Scott Wlaschin
 
Lambda and Stream Master class - part 1
Lambda and Stream Master class - part 1Lambda and Stream Master class - part 1
Lambda and Stream Master class - part 1
José Paumard
 
Key-Value Stores: a practical overview
Key-Value Stores: a practical overviewKey-Value Stores: a practical overview
Key-Value Stores: a practical overviewMarc Seeger
 
PostgreSQL - Lección 6 - Subconsultas
PostgreSQL - Lección 6 - SubconsultasPostgreSQL - Lección 6 - Subconsultas
PostgreSQL - Lección 6 - SubconsultasNicola Strappazzon C.
 
Sql injection
Sql injectionSql injection
Sql injection
Mehul Boghra
 
Introduction to Mongodb execution plan and optimizer
Introduction to Mongodb execution plan and optimizerIntroduction to Mongodb execution plan and optimizer
Introduction to Mongodb execution plan and optimizer
Mydbops
 
My sql events
My sql eventsMy sql events
My sql events
vijayakumargvk
 
MongodB Internals
MongodB InternalsMongodB Internals
MongodB Internals
Norberto Leite
 
Python Deserialization Attacks
Python Deserialization AttacksPython Deserialization Attacks
Python Deserialization Attacks
NSConclave
 
Angular & RXJS: examples and use cases
Angular & RXJS: examples and use casesAngular & RXJS: examples and use cases
Angular & RXJS: examples and use cases
Fabio Biondi
 
Flask Basics
Flask BasicsFlask Basics
Flask Basics
Eueung Mulyana
 
RxJS Evolved
RxJS EvolvedRxJS Evolved
RxJS Evolved
trxcllnt
 
RxJS - The Basics & The Future
RxJS - The Basics & The FutureRxJS - The Basics & The Future
RxJS - The Basics & The Future
Tracy Lee
 
MongoDB Europe 2016 - Advanced MongoDB Aggregation Pipelines
MongoDB Europe 2016 - Advanced MongoDB Aggregation PipelinesMongoDB Europe 2016 - Advanced MongoDB Aggregation Pipelines
MongoDB Europe 2016 - Advanced MongoDB Aggregation Pipelines
MongoDB
 
Angular Pipes Workshop
Angular Pipes WorkshopAngular Pipes Workshop
Angular Pipes Workshop
Nir Kaufman
 

What's hot (20)

Writing and using Hamcrest Matchers
Writing and using Hamcrest MatchersWriting and using Hamcrest Matchers
Writing and using Hamcrest Matchers
 
JSON-LD and MongoDB
JSON-LD and MongoDBJSON-LD and MongoDB
JSON-LD and MongoDB
 
Spring security
Spring securitySpring security
Spring security
 
Indexing with MongoDB
Indexing with MongoDBIndexing with MongoDB
Indexing with MongoDB
 
Introduction to Activiti BPM
Introduction to Activiti BPMIntroduction to Activiti BPM
Introduction to Activiti BPM
 
Railway Oriented Programming
Railway Oriented ProgrammingRailway Oriented Programming
Railway Oriented Programming
 
Lambda and Stream Master class - part 1
Lambda and Stream Master class - part 1Lambda and Stream Master class - part 1
Lambda and Stream Master class - part 1
 
Key-Value Stores: a practical overview
Key-Value Stores: a practical overviewKey-Value Stores: a practical overview
Key-Value Stores: a practical overview
 
PostgreSQL - Lección 6 - Subconsultas
PostgreSQL - Lección 6 - SubconsultasPostgreSQL - Lección 6 - Subconsultas
PostgreSQL - Lección 6 - Subconsultas
 
Sql injection
Sql injectionSql injection
Sql injection
 
Introduction to Mongodb execution plan and optimizer
Introduction to Mongodb execution plan and optimizerIntroduction to Mongodb execution plan and optimizer
Introduction to Mongodb execution plan and optimizer
 
My sql events
My sql eventsMy sql events
My sql events
 
MongodB Internals
MongodB InternalsMongodB Internals
MongodB Internals
 
Python Deserialization Attacks
Python Deserialization AttacksPython Deserialization Attacks
Python Deserialization Attacks
 
Angular & RXJS: examples and use cases
Angular & RXJS: examples and use casesAngular & RXJS: examples and use cases
Angular & RXJS: examples and use cases
 
Flask Basics
Flask BasicsFlask Basics
Flask Basics
 
RxJS Evolved
RxJS EvolvedRxJS Evolved
RxJS Evolved
 
RxJS - The Basics & The Future
RxJS - The Basics & The FutureRxJS - The Basics & The Future
RxJS - The Basics & The Future
 
MongoDB Europe 2016 - Advanced MongoDB Aggregation Pipelines
MongoDB Europe 2016 - Advanced MongoDB Aggregation PipelinesMongoDB Europe 2016 - Advanced MongoDB Aggregation Pipelines
MongoDB Europe 2016 - Advanced MongoDB Aggregation Pipelines
 
Angular Pipes Workshop
Angular Pipes WorkshopAngular Pipes Workshop
Angular Pipes Workshop
 

Similar to CQRS and Event Sourcing with MongoDB and PHP

Flexible Event Tracking (Paul Gebheim)
Flexible Event Tracking (Paul Gebheim)Flexible Event Tracking (Paul Gebheim)
Flexible Event Tracking (Paul Gebheim)MongoSF
 
Building Microservices with Scala, functional domain models and Spring Boot -...
Building Microservices with Scala, functional domain models and Spring Boot -...Building Microservices with Scala, functional domain models and Spring Boot -...
Building Microservices with Scala, functional domain models and Spring Boot -...
JAXLondon2014
 
#JaxLondon: Building microservices with Scala, functional domain models and S...
#JaxLondon: Building microservices with Scala, functional domain models and S...#JaxLondon: Building microservices with Scala, functional domain models and S...
#JaxLondon: Building microservices with Scala, functional domain models and S...
Chris Richardson
 
Log everything! @DC13
Log everything! @DC13Log everything! @DC13
Log everything! @DC13
DECK36
 
NoSQL Deepdive - with Informix NoSQL. IOD 2013
NoSQL Deepdive - with Informix NoSQL. IOD 2013NoSQL Deepdive - with Informix NoSQL. IOD 2013
NoSQL Deepdive - with Informix NoSQL. IOD 2013
Keshav Murthy
 
Snowplow: evolve your analytics stack with your business
Snowplow: evolve your analytics stack with your businessSnowplow: evolve your analytics stack with your business
Snowplow: evolve your analytics stack with your business
yalisassoon
 
Grokking Engineering - Data Analytics Infrastructure at Viki - Huy Nguyen
Grokking Engineering - Data Analytics Infrastructure at Viki - Huy NguyenGrokking Engineering - Data Analytics Infrastructure at Viki - Huy Nguyen
Grokking Engineering - Data Analytics Infrastructure at Viki - Huy Nguyen
Huy Nguyen
 
Building Your First App with MongoDB Stitch
Building Your First App with MongoDB StitchBuilding Your First App with MongoDB Stitch
Building Your First App with MongoDB Stitch
MongoDB
 
Cloudbase.io MoSync Reload Course
Cloudbase.io MoSync Reload CourseCloudbase.io MoSync Reload Course
Cloudbase.io MoSync Reload Course
cloudbase.io
 
Our Data Ourselves, Pydata 2015
Our Data Ourselves, Pydata 2015Our Data Ourselves, Pydata 2015
Our Data Ourselves, Pydata 2015
kingsBSD
 
The Fine Art of Time Travelling: implementing Event Sourcing
The Fine Art of Time Travelling: implementing Event SourcingThe Fine Art of Time Travelling: implementing Event Sourcing
The Fine Art of Time Travelling: implementing Event Sourcing
Andrea Saltarello
 
Meet with Meteor
Meet with MeteorMeet with Meteor
Meet with Meteor
Tahmina Khatoon
 
Snowplow - Evolve your analytics stack with your business
Snowplow - Evolve your analytics stack with your businessSnowplow - Evolve your analytics stack with your business
Snowplow - Evolve your analytics stack with your business
Giuseppe Gaviani
 
Tutorial: Building Your First App with MongoDB Stitch
Tutorial: Building Your First App with MongoDB StitchTutorial: Building Your First App with MongoDB Stitch
Tutorial: Building Your First App with MongoDB Stitch
MongoDB
 
Building a system for machine and event-oriented data - Data Day Seattle 2015
Building a system for machine and event-oriented data - Data Day Seattle 2015Building a system for machine and event-oriented data - Data Day Seattle 2015
Building a system for machine and event-oriented data - Data Day Seattle 2015
Eric Sammer
 
Social Analytics with MongoDB
Social Analytics with MongoDBSocial Analytics with MongoDB
Social Analytics with MongoDBPatrick Stokes
 
Building and deploying microservices with event sourcing, CQRS and Docker (Ha...
Building and deploying microservices with event sourcing, CQRS and Docker (Ha...Building and deploying microservices with event sourcing, CQRS and Docker (Ha...
Building and deploying microservices with event sourcing, CQRS and Docker (Ha...
Chris Richardson
 
Designing The Right Schema To Power Heap (PGConf Silicon Valley 2016)
Designing The Right Schema To Power Heap (PGConf Silicon Valley 2016)Designing The Right Schema To Power Heap (PGConf Silicon Valley 2016)
Designing The Right Schema To Power Heap (PGConf Silicon Valley 2016)
Dan Robinson
 
The Future of Streaming: Global Apps, Event Stores and Serverless
The Future of Streaming: Global Apps, Event Stores and ServerlessThe Future of Streaming: Global Apps, Event Stores and Serverless
The Future of Streaming: Global Apps, Event Stores and Serverless
Ben Stopford
 
Building a system for machine and event-oriented data - Velocity, Santa Clara...
Building a system for machine and event-oriented data - Velocity, Santa Clara...Building a system for machine and event-oriented data - Velocity, Santa Clara...
Building a system for machine and event-oriented data - Velocity, Santa Clara...
Eric Sammer
 

Similar to CQRS and Event Sourcing with MongoDB and PHP (20)

Flexible Event Tracking (Paul Gebheim)
Flexible Event Tracking (Paul Gebheim)Flexible Event Tracking (Paul Gebheim)
Flexible Event Tracking (Paul Gebheim)
 
Building Microservices with Scala, functional domain models and Spring Boot -...
Building Microservices with Scala, functional domain models and Spring Boot -...Building Microservices with Scala, functional domain models and Spring Boot -...
Building Microservices with Scala, functional domain models and Spring Boot -...
 
#JaxLondon: Building microservices with Scala, functional domain models and S...
#JaxLondon: Building microservices with Scala, functional domain models and S...#JaxLondon: Building microservices with Scala, functional domain models and S...
#JaxLondon: Building microservices with Scala, functional domain models and S...
 
Log everything! @DC13
Log everything! @DC13Log everything! @DC13
Log everything! @DC13
 
NoSQL Deepdive - with Informix NoSQL. IOD 2013
NoSQL Deepdive - with Informix NoSQL. IOD 2013NoSQL Deepdive - with Informix NoSQL. IOD 2013
NoSQL Deepdive - with Informix NoSQL. IOD 2013
 
Snowplow: evolve your analytics stack with your business
Snowplow: evolve your analytics stack with your businessSnowplow: evolve your analytics stack with your business
Snowplow: evolve your analytics stack with your business
 
Grokking Engineering - Data Analytics Infrastructure at Viki - Huy Nguyen
Grokking Engineering - Data Analytics Infrastructure at Viki - Huy NguyenGrokking Engineering - Data Analytics Infrastructure at Viki - Huy Nguyen
Grokking Engineering - Data Analytics Infrastructure at Viki - Huy Nguyen
 
Building Your First App with MongoDB Stitch
Building Your First App with MongoDB StitchBuilding Your First App with MongoDB Stitch
Building Your First App with MongoDB Stitch
 
Cloudbase.io MoSync Reload Course
Cloudbase.io MoSync Reload CourseCloudbase.io MoSync Reload Course
Cloudbase.io MoSync Reload Course
 
Our Data Ourselves, Pydata 2015
Our Data Ourselves, Pydata 2015Our Data Ourselves, Pydata 2015
Our Data Ourselves, Pydata 2015
 
The Fine Art of Time Travelling: implementing Event Sourcing
The Fine Art of Time Travelling: implementing Event SourcingThe Fine Art of Time Travelling: implementing Event Sourcing
The Fine Art of Time Travelling: implementing Event Sourcing
 
Meet with Meteor
Meet with MeteorMeet with Meteor
Meet with Meteor
 
Snowplow - Evolve your analytics stack with your business
Snowplow - Evolve your analytics stack with your businessSnowplow - Evolve your analytics stack with your business
Snowplow - Evolve your analytics stack with your business
 
Tutorial: Building Your First App with MongoDB Stitch
Tutorial: Building Your First App with MongoDB StitchTutorial: Building Your First App with MongoDB Stitch
Tutorial: Building Your First App with MongoDB Stitch
 
Building a system for machine and event-oriented data - Data Day Seattle 2015
Building a system for machine and event-oriented data - Data Day Seattle 2015Building a system for machine and event-oriented data - Data Day Seattle 2015
Building a system for machine and event-oriented data - Data Day Seattle 2015
 
Social Analytics with MongoDB
Social Analytics with MongoDBSocial Analytics with MongoDB
Social Analytics with MongoDB
 
Building and deploying microservices with event sourcing, CQRS and Docker (Ha...
Building and deploying microservices with event sourcing, CQRS and Docker (Ha...Building and deploying microservices with event sourcing, CQRS and Docker (Ha...
Building and deploying microservices with event sourcing, CQRS and Docker (Ha...
 
Designing The Right Schema To Power Heap (PGConf Silicon Valley 2016)
Designing The Right Schema To Power Heap (PGConf Silicon Valley 2016)Designing The Right Schema To Power Heap (PGConf Silicon Valley 2016)
Designing The Right Schema To Power Heap (PGConf Silicon Valley 2016)
 
The Future of Streaming: Global Apps, Event Stores and Serverless
The Future of Streaming: Global Apps, Event Stores and ServerlessThe Future of Streaming: Global Apps, Event Stores and Serverless
The Future of Streaming: Global Apps, Event Stores and Serverless
 
Building a system for machine and event-oriented data - Velocity, Santa Clara...
Building a system for machine and event-oriented data - Velocity, Santa Clara...Building a system for machine and event-oriented data - Velocity, Santa Clara...
Building a system for machine and event-oriented data - Velocity, Santa Clara...
 

More from Davide Bellettini

Presentazione UniversiBO
Presentazione UniversiBOPresentazione UniversiBO
Presentazione UniversiBO
Davide Bellettini
 
Presentazione UniversiBO (Ingegneria Informatica)
Presentazione UniversiBO (Ingegneria Informatica)Presentazione UniversiBO (Ingegneria Informatica)
Presentazione UniversiBO (Ingegneria Informatica)Davide Bellettini
 
Riunione aperta UniversiBO 08/05/2012
Riunione aperta UniversiBO 08/05/2012Riunione aperta UniversiBO 08/05/2012
Riunione aperta UniversiBO 08/05/2012
Davide Bellettini
 
Framework di supporto allo sviluppo di applicazioni Web
Framework di supporto allo sviluppo di applicazioni WebFramework di supporto allo sviluppo di applicazioni Web
Framework di supporto allo sviluppo di applicazioni Web
Davide Bellettini
 

More from Davide Bellettini (6)

Presentazione UniversiBO
Presentazione UniversiBOPresentazione UniversiBO
Presentazione UniversiBO
 
Presentazione UniversiBO (Ingegneria Informatica)
Presentazione UniversiBO (Ingegneria Informatica)Presentazione UniversiBO (Ingegneria Informatica)
Presentazione UniversiBO (Ingegneria Informatica)
 
pugBO #10 PSR e Composer
pugBO #10 PSR e ComposerpugBO #10 PSR e Composer
pugBO #10 PSR e Composer
 
Riunione aperta UniversiBO 08/05/2012
Riunione aperta UniversiBO 08/05/2012Riunione aperta UniversiBO 08/05/2012
Riunione aperta UniversiBO 08/05/2012
 
MyJOrganizer presentazione
MyJOrganizer presentazioneMyJOrganizer presentazione
MyJOrganizer presentazione
 
Framework di supporto allo sviluppo di applicazioni Web
Framework di supporto allo sviluppo di applicazioni WebFramework di supporto allo sviluppo di applicazioni Web
Framework di supporto allo sviluppo di applicazioni Web
 

Recently uploaded

Dominate Social Media with TubeTrivia AI’s Addictive Quiz Videos.pdf
Dominate Social Media with TubeTrivia AI’s Addictive Quiz Videos.pdfDominate Social Media with TubeTrivia AI’s Addictive Quiz Videos.pdf
Dominate Social Media with TubeTrivia AI’s Addictive Quiz Videos.pdf
AMB-Review
 
Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...
Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...
Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...
Globus
 
Designing for Privacy in Amazon Web Services
Designing for Privacy in Amazon Web ServicesDesigning for Privacy in Amazon Web Services
Designing for Privacy in Amazon Web Services
KrzysztofKkol1
 
Providing Globus Services to Users of JASMIN for Environmental Data Analysis
Providing Globus Services to Users of JASMIN for Environmental Data AnalysisProviding Globus Services to Users of JASMIN for Environmental Data Analysis
Providing Globus Services to Users of JASMIN for Environmental Data Analysis
Globus
 
2024 RoOUG Security model for the cloud.pptx
2024 RoOUG Security model for the cloud.pptx2024 RoOUG Security model for the cloud.pptx
2024 RoOUG Security model for the cloud.pptx
Georgi Kodinov
 
Using IESVE for Room Loads Analysis - Australia & New Zealand
Using IESVE for Room Loads Analysis - Australia & New ZealandUsing IESVE for Room Loads Analysis - Australia & New Zealand
Using IESVE for Room Loads Analysis - Australia & New Zealand
IES VE
 
WSO2Con2024 - WSO2's IAM Vision: Identity-Led Digital Transformation
WSO2Con2024 - WSO2's IAM Vision: Identity-Led Digital TransformationWSO2Con2024 - WSO2's IAM Vision: Identity-Led Digital Transformation
WSO2Con2024 - WSO2's IAM Vision: Identity-Led Digital Transformation
WSO2
 
In 2015, I used to write extensions for Joomla, WordPress, phpBB3, etc and I ...
In 2015, I used to write extensions for Joomla, WordPress, phpBB3, etc and I ...In 2015, I used to write extensions for Joomla, WordPress, phpBB3, etc and I ...
In 2015, I used to write extensions for Joomla, WordPress, phpBB3, etc and I ...
Juraj Vysvader
 
BoxLang: Review our Visionary Licenses of 2024
BoxLang: Review our Visionary Licenses of 2024BoxLang: Review our Visionary Licenses of 2024
BoxLang: Review our Visionary Licenses of 2024
Ortus Solutions, Corp
 
Visitor Management System in India- Vizman.app
Visitor Management System in India- Vizman.appVisitor Management System in India- Vizman.app
Visitor Management System in India- Vizman.app
NaapbooksPrivateLimi
 
Advanced Flow Concepts Every Developer Should Know
Advanced Flow Concepts Every Developer Should KnowAdvanced Flow Concepts Every Developer Should Know
Advanced Flow Concepts Every Developer Should Know
Peter Caitens
 
Lecture 1 Introduction to games development
Lecture 1 Introduction to games developmentLecture 1 Introduction to games development
Lecture 1 Introduction to games development
abdulrafaychaudhry
 
Quarkus Hidden and Forbidden Extensions
Quarkus Hidden and Forbidden ExtensionsQuarkus Hidden and Forbidden Extensions
Quarkus Hidden and Forbidden Extensions
Max Andersen
 
Explore Modern SharePoint Templates for 2024
Explore Modern SharePoint Templates for 2024Explore Modern SharePoint Templates for 2024
Explore Modern SharePoint Templates for 2024
Sharepoint Designs
 
Innovating Inference - Remote Triggering of Large Language Models on HPC Clus...
Innovating Inference - Remote Triggering of Large Language Models on HPC Clus...Innovating Inference - Remote Triggering of Large Language Models on HPC Clus...
Innovating Inference - Remote Triggering of Large Language Models on HPC Clus...
Globus
 
First Steps with Globus Compute Multi-User Endpoints
First Steps with Globus Compute Multi-User EndpointsFirst Steps with Globus Compute Multi-User Endpoints
First Steps with Globus Compute Multi-User Endpoints
Globus
 
Globus Connect Server Deep Dive - GlobusWorld 2024
Globus Connect Server Deep Dive - GlobusWorld 2024Globus Connect Server Deep Dive - GlobusWorld 2024
Globus Connect Server Deep Dive - GlobusWorld 2024
Globus
 
Why React Native as a Strategic Advantage for Startup Innovation.pdf
Why React Native as a Strategic Advantage for Startup Innovation.pdfWhy React Native as a Strategic Advantage for Startup Innovation.pdf
Why React Native as a Strategic Advantage for Startup Innovation.pdf
ayushiqss
 
Globus Compute Introduction - GlobusWorld 2024
Globus Compute Introduction - GlobusWorld 2024Globus Compute Introduction - GlobusWorld 2024
Globus Compute Introduction - GlobusWorld 2024
Globus
 
Into the Box 2024 - Keynote Day 2 Slides.pdf
Into the Box 2024 - Keynote Day 2 Slides.pdfInto the Box 2024 - Keynote Day 2 Slides.pdf
Into the Box 2024 - Keynote Day 2 Slides.pdf
Ortus Solutions, Corp
 

Recently uploaded (20)

Dominate Social Media with TubeTrivia AI’s Addictive Quiz Videos.pdf
Dominate Social Media with TubeTrivia AI’s Addictive Quiz Videos.pdfDominate Social Media with TubeTrivia AI’s Addictive Quiz Videos.pdf
Dominate Social Media with TubeTrivia AI’s Addictive Quiz Videos.pdf
 
Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...
Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...
Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...
 
Designing for Privacy in Amazon Web Services
Designing for Privacy in Amazon Web ServicesDesigning for Privacy in Amazon Web Services
Designing for Privacy in Amazon Web Services
 
Providing Globus Services to Users of JASMIN for Environmental Data Analysis
Providing Globus Services to Users of JASMIN for Environmental Data AnalysisProviding Globus Services to Users of JASMIN for Environmental Data Analysis
Providing Globus Services to Users of JASMIN for Environmental Data Analysis
 
2024 RoOUG Security model for the cloud.pptx
2024 RoOUG Security model for the cloud.pptx2024 RoOUG Security model for the cloud.pptx
2024 RoOUG Security model for the cloud.pptx
 
Using IESVE for Room Loads Analysis - Australia & New Zealand
Using IESVE for Room Loads Analysis - Australia & New ZealandUsing IESVE for Room Loads Analysis - Australia & New Zealand
Using IESVE for Room Loads Analysis - Australia & New Zealand
 
WSO2Con2024 - WSO2's IAM Vision: Identity-Led Digital Transformation
WSO2Con2024 - WSO2's IAM Vision: Identity-Led Digital TransformationWSO2Con2024 - WSO2's IAM Vision: Identity-Led Digital Transformation
WSO2Con2024 - WSO2's IAM Vision: Identity-Led Digital Transformation
 
In 2015, I used to write extensions for Joomla, WordPress, phpBB3, etc and I ...
In 2015, I used to write extensions for Joomla, WordPress, phpBB3, etc and I ...In 2015, I used to write extensions for Joomla, WordPress, phpBB3, etc and I ...
In 2015, I used to write extensions for Joomla, WordPress, phpBB3, etc and I ...
 
BoxLang: Review our Visionary Licenses of 2024
BoxLang: Review our Visionary Licenses of 2024BoxLang: Review our Visionary Licenses of 2024
BoxLang: Review our Visionary Licenses of 2024
 
Visitor Management System in India- Vizman.app
Visitor Management System in India- Vizman.appVisitor Management System in India- Vizman.app
Visitor Management System in India- Vizman.app
 
Advanced Flow Concepts Every Developer Should Know
Advanced Flow Concepts Every Developer Should KnowAdvanced Flow Concepts Every Developer Should Know
Advanced Flow Concepts Every Developer Should Know
 
Lecture 1 Introduction to games development
Lecture 1 Introduction to games developmentLecture 1 Introduction to games development
Lecture 1 Introduction to games development
 
Quarkus Hidden and Forbidden Extensions
Quarkus Hidden and Forbidden ExtensionsQuarkus Hidden and Forbidden Extensions
Quarkus Hidden and Forbidden Extensions
 
Explore Modern SharePoint Templates for 2024
Explore Modern SharePoint Templates for 2024Explore Modern SharePoint Templates for 2024
Explore Modern SharePoint Templates for 2024
 
Innovating Inference - Remote Triggering of Large Language Models on HPC Clus...
Innovating Inference - Remote Triggering of Large Language Models on HPC Clus...Innovating Inference - Remote Triggering of Large Language Models on HPC Clus...
Innovating Inference - Remote Triggering of Large Language Models on HPC Clus...
 
First Steps with Globus Compute Multi-User Endpoints
First Steps with Globus Compute Multi-User EndpointsFirst Steps with Globus Compute Multi-User Endpoints
First Steps with Globus Compute Multi-User Endpoints
 
Globus Connect Server Deep Dive - GlobusWorld 2024
Globus Connect Server Deep Dive - GlobusWorld 2024Globus Connect Server Deep Dive - GlobusWorld 2024
Globus Connect Server Deep Dive - GlobusWorld 2024
 
Why React Native as a Strategic Advantage for Startup Innovation.pdf
Why React Native as a Strategic Advantage for Startup Innovation.pdfWhy React Native as a Strategic Advantage for Startup Innovation.pdf
Why React Native as a Strategic Advantage for Startup Innovation.pdf
 
Globus Compute Introduction - GlobusWorld 2024
Globus Compute Introduction - GlobusWorld 2024Globus Compute Introduction - GlobusWorld 2024
Globus Compute Introduction - GlobusWorld 2024
 
Into the Box 2024 - Keynote Day 2 Slides.pdf
Into the Box 2024 - Keynote Day 2 Slides.pdfInto the Box 2024 - Keynote Day 2 Slides.pdf
Into the Box 2024 - Keynote Day 2 Slides.pdf
 

CQRS and Event Sourcing with MongoDB and PHP

  • 1. CQRS and Event Sourcing with MongoDB and PHP
  • 2. About me Davide Bellettini ● Developer at Onebip ● TDD addicted @SbiellONE — about.bellettini.me
  • 3. What is this talk about
  • 4. A little bit of context
  • 5. About Onebip Mobile payment platform. Start-up born in 2005, acquired by Neomobile group in 2011. Onebip today: - 70 countries - 200+ carriers - 5 billions potential users
  • 6. LAMP stack It all started with a Monolith
  • 7. self-contained services communicating via REST To a distributed system
  • 8. First class modern NoSQL distributed dbs Modern services
  • 9. But the Monolith is still there
  • 10. The problem A reporting horror story
  • 11. We need three new reports! ― Manager Sure, no problem!
  • 12. Deal with the legacy SQL schema
  • 13.
  • 15. A little bit of queries here, a little bit of map-reduce there
  • 19. Your queries are killing production! ― SysAdmin
  • 20. Still not enough! Heavy query optimization, adding indexes
  • 21. Let’s reuse data from other reports (don’t do that)
  • 22. DB is ok, reports delivered.
  • 24. Houston, we have a problem. Reports are not consistent (with other reports) ― Business guy
  • 25.
  • 28. It’s hard to compare different data in a distributed system splitted across multiple domains #1 Avoid multiple sources of truth
  • 29. Same words, different concepts across domains #2 Ubiquitous language
  • 30. Changing a report shouldn’t have side effects #3 Fault tolerance to change
  • 33. #2 Data Warehouse + Consultants
  • 36. No downtime in production Consistent across domains Must have
  • 37. A system elastic enough to extract any metric Real time data Nice to have
  • 38. In DDD we found the light
  • 39. CQRS and Event Sourcing
  • 41.
  • 42. Commands Anything that happens in one of your domains is triggered by a command and generates one or more events. Order received -> payment sent -> Items queued -> Confirmation email sent
  • 43. Query Generate read models from events depending how data need to be actually used (by users and other application internals)
  • 44. Event Sourcing The fundamental idea of Event Sourcing is that of ensuring every change to the state of an application is captured in an event object, and that these event objects are themselves stored in the sequence they were applied. ― Martin Fowler
  • 45. Starting from the beginning of time, you are literally unrolling history to reach state in a given time Unrolling a stream of events
  • 46. Idea #1 Every change to the state of your application is captured in event object. “UserLoggedIn”, “PaymentSent”, “UserLanded”
  • 47. Idea #2 Events are stored in the sequence they were applied inside an event store
  • 48. Idea #3 Everything is an event. No more state.
  • 49. Idea #4 One way to store data/events but potentially infinite ways to read them. A practical example Tech ops, business control, monitoring, accounting they all are interested in reading data from different views.
  • 51. You start with this { "_id": ObjectId("123"), "username": "Flash", "city": …, "phone": …, "email": …, }
  • 52. The more successful your company is, the more people … The more people, the more views
  • 53. With documental dbs it's magically easy to add new fields to your collections.
  • 54. Soon you might end up with { "_id": ObjectId("123"), "username": "Flash", "city": …, "phone": …, "email": …, "created_at": …, "updated_at": …, "ever_tried_to_purchase_something": …, "canceled_at": …, "acquisition_channel": …, "terminated_at": …, "latest_purchase_date": …, … }
  • 55. A bomb waiting to detonate It’s impossible to keep adding state changes to your documents and then expect to be able to extract them with a single query.
  • 57. Event Store ● Engineered for event sourcing ● Supports projections ● By the father of CQRS (Greg Young) ● Great performances http://geteventstore.com/ The bad Based on Mono, still too unstable.
  • 58. LevelWHEN An event store built with Node.js and LevelDB ● Faster than light ● Completely custom, no tools to handle aggregates https://github.com/gabrielelana/levelWHEN
  • 59. The known path ● PHP (any other language would just do fine) ● MongoDB 2.2.x
  • 60. Why MongoDB Events are not relational Scales well Awesome aggregation framework
  • 63. Service | | [event payload] | | Service --- Queue System <------------> API -> MongoDB / | / [event payload] | / | Service | The write architecture
  • 65. MongoDB replica set A MongoDB replica set with two logical dbs: 1. Event store where we would store events 2. Reporting DB where we would store aggregates and final reports
  • 66. Anatomy of an event 1/2 { '_id' : '3318c11e-fe60-4c80-a2b2-7add681492d9', 'type': 'an-event-type', 'data': { 'meta' : { … }, 'payload' : { … } } }
  • 67. Anatomy of an event 2/2 'meta' : { 'creation_date': ISODate("2014-21-11T00:00:01Z"), 'saved_date': ISODate("2014-21-11T00:00:02Z"), 'source': 'some-bounded-context', 'correlation_id': 'a-correlation-id' }, 'payload' : { 'user_id': '1234', 'animal': 'unicorn', 'colour': 'pink', 'purchase_date': ISODate("2014-21-11T00:00:00Z"), 'price': '20/fantaeuros' }
  • 68. Don’t trust the network: Idempotence { '_id' : '3318c11e-fe60-4c80-a2b2-7add681492d9', … } The _id field is actually defined client side and ensures idempotence if an event is received two times
  • 69. Indexes ● Events collection is huge (~100*N documents) ● Use indexes wisely as they are necessary yet expensive ● With suggested event structure: {‘data.meta.created_at’: 1, type:1}
  • 70. Benchmarking How many events/second can you store? Our machines were able to store roughly 150 events/sec. This number can be greatly increased with dedicated IOPS, more aggressive inserting policies, etc...
  • 71. Final tips ● Use SSD on your storage machines ● Pay attention to write concerns (w=majority) ● Test your replica set fault tolerance
  • 73. Sequential Projector -> Event Mapper -> Projection -> Aggregation The event processing pipeline
  • 74. A real life problem What is the conversion rate of our registered users?
  • 75. #1 The registration event { '_id' : '3318c11e-fe60-4c80-a2b2-7add681492d9', 'type': 'user-registered', 'data': { 'meta' : { 'save_date': ISODate("2014-21-11T00:00:09Z"), 'created_at': ISODate("2014-21-11T00:00:01Z"), 'source': 'core-domain', 'correlation_id': 'user-123456' }, 'payload' : { 'user_id': 123, 'username': 'flash', 'email': 'a-dummy-email@gmail.com', 'country': 'IT' } }
  • 76. #2 The purchase event { '_id' : '3318c11e-fe60-4c80-a2b2-7add681492d9', 'type': 'user-purchased', 'data': { 'meta' : { 'save_date': ISODate("2014-21-11T00:10:09Z"), 'created_at': ISODate("2014-21-11T00:10:01Z"), 'source': 'payment-gateway', 'correlation_id': 'user-123456' }, 'payload' : { 'user_id': 123, 'email': 'a-dummy-email@gmail.com', 'amount': 20, 'value': EUR, 'payment': 'credit_card', 'item': 'fluffy cat' } }
  • 77. Sequential projector 1/2 []->[x]->[]->[x]->[]->[]->[]->[] |--------------| |------------| | | | | ---> Projector Divides the stream of events into batches, filters events by type and pass those of interest to the mapper
  • 78. Sequential projector 2/2 ● It’s a good idea to select fixed sizes batches to avoid memory problems when you load your Cursor in memory ● Could be a long-running process selecting events as they arrive in realtime
  • 79. Event mapper 1/3 Translates event fields to the Read Model domain Takes an event as input, applies a bunch of logic and will return a list of Read Model fields.
  • 80. Event mapper 2/3 Input event: user-registered Output: $output = [ 'user_id' => 123, // simply copied 'user_name' => 'flash', // simply copied 'email' => 'a-dummy-email@gmail.com', // simply copied 'registered_at' => "2014-21-11T00:00:01Z" // From the data.meta.created_at event field ];
  • 81. Event mapper 3/3 Input event: user-purchased Output: $output = [ 'user_id' => 123, // simply copied 'email' => 'a-dummy-email@gmail.com', // simply copied 'purchased_at': "2014-21-11T00:10:01Z" // From the data.meta.created_at event field ];
  • 82. Projection Essentially it is your read model. The data that the business is interested in.
  • 83. The Projection after event #1 db.users_conversion_rate_projection.findOne() { 'user_id': 123, 'user_name': 'flash', 'email': 'a-dummy-email@gmail.com', 'registered_at': "2014-21-11T00:00:01Z" }
  • 84. The Projection after event #2 { 'user_id': 123, 'user_name': 'flash', 'email': 'a-dummy-email@gmail.com', 'registered_at': "2014-21-11T00:00:01Z" 'purchased_at': "2014-21-11" // Added this field and rewrote others }
  • 85. The Projection collection{ 'user_id': 123, 'user_name': 'flash', 'email': 'a-dummy-email@gmail.com', 'registered_at': "2014-21-11", 'purchased_at': "2014-21-11" // Added this field and rewrote others } { 'user_id': 456, 'user_name': 'batman', 'email': 'a-dummy-email@gmail.com', 'registered_at': "2014-21-11", 'purchased_at': "2014-21-11" // Added this field and rewrote others } { 'user_id': 789, 'user_name': 'superman', 'email': 'a-dummy-email@gmail.com', 'registered_at': "2014-21-12", 'purchased_at': "2014-21-12" // Added this field and rewrote others }
  • 86. The Projection - A few thoughts Note that we didn't copy from events to projection all the available fields. Just relevant ones.
  • 87. From these two events we could have generated infinite read models such as ● List all purchased products and related amounts for the company buyers ● Map all sales and revenues for our accounting dept ● List transactions for the financial department
  • 88. One way to write, infinite ways to read!
  • 89. The aggregation (1) - Total registered users var registered = db.users_conversion_rate_projection.aggregate([ { $match: { "registered_at": { $gte: ISODate("2015-11-21"), $lte: ISODate("2015-11-22") } } }, { $group: { _id: { }, count: { $sum:1 } } } ]);
  • 90. The aggregation (2) - User with a purchase var purchased = db.users_conversion_rate_projection.aggregate([ { $match: { "registered_at": { $gte: ISODate("2015-11-21"), $lte: ISODate("2015-11-22") }, "purchased_at": { $exists: true } } }, { $group: { _id: { }, count: { $sum:1 } } } ]);
  • 91. The aggregation (3) - Automate all the things ● You can easily create the aggregation framework statement by composition abstracting the concept of Column. ● This way you can dynamically aggregate your projections on (for example) an API requests. ● If your Projector is a long running process, your projections will be updated to the second and you automagically get realtime data.
  • 92. Another events usage: Business & Tech Monitoring
  • 93. Beware of the beast! No Silver Bullet
  • 94. Events are expensive They require a lot of TIME to be parsed
  • 95. Events are expensive You will end up with this billion size collection (and counting)
  • 96. Fixing wrong events is painful
  • 98. Moving around events is horribly painful
  • 99. Actually it will make your life incredibly difficult with hidden bugs and leaking documentation. Mongo won’t help you
  • 100. Improvements ● Upgrade from MongoDB 2.2.x to 3.0.x ● Switch to WiredTiger storage engine to save space
  • 101. Credits Based on a talk by Jacopo Nardiello ● Slides: http://bit.ly/es-nardiello-2014 ● Video: https://vimeo.com/113370688
  • 102. Q&A