Behind OOD
// domain modelling in a post-OO world
Ruslan Shevchenko
Lynx Capital Partners
https://github.com/rssh
@rssh1
Domain modelling
• Representation of business domain objects:
• in ‘head’ of people
• in code
Domain modelling.
• Outline typical OOD issues
• Build
• simple domain model for toy billing system.
// scala, can be mapped to java.
• internal DSL
Domain modelling
• Traditional OO way: have layer of classes,
which corresponds to domain objects.
• Extensibility via inheritance in some
consistent Universal Ontology
• Intensional Equality [identity != attribute]
• Object instance <=> Entity in real world
Traditional OO WAY
• Human is an upright, featherless biped with
broad, flat nails.
Traditional OO WAY
• Human is an upright, featherless biped with
broad, flat nails.
Open for extensions close for modifications
Traditional OO WAY
• Human is an upright, featherless biped with
broad, flat nails.
Open for extensions close for modifications
Traditional OO WAY
• Intensional Equality [ mutability ]
// same identity thought lifecycle
Traditional OO WAY
• Object in code <=> Object in real world
class Person {
int getId();
String getName();
int getAge();
Set<Person> getChilds()
}
— thread-safe ?
— persistent updates ?
Domain modelling
• Traditional OO way: have layer of classes,
which corresponds to domain objects.
• Extensibility via inheritance in some
consistent Universal Ontology
• Intensional Equality [identity != attribute]
• Object instance <=> Entity in real world
Domain modelling
• Traditional OO way: similar to early
philosophy
• Very general
• Idealistic
• Fit to concrete cases may be undefined
Domain modelling
• Post OO way: describe limited set of objects
and relationships.
• Algebra instead Ontology
• Existential equality [identity == same
attributes]
• Rules of algebra <=> rules of reality.
Domain modelling: where to start.
• Example: description of small billing system
Subscriber
Billing System
Service PaymentPlanuse
in accordance
with
Service is Internet | Telephony
PaymentPlan is Monthly Fee for Quantity Limit and
Overhead cost per Unit
Unit Internet is Gbin
Telephony is Minute
and Bandwidth
TariffPlan: Use limit and quantity from service.
sealed trait Service
{
type Limits
def quantity(l:Limits): BigDecimal
}
case object Internet extends Service
{
case class Limits(gb:Int,bandwidth:Int)
def quantity(l:Limits) = l.gb
}
case object Telephony extends Service
{
type Limits = Int
def quantity(l) = l
TariffPlan: Price per limit and charge
case class TariffPlan[Limits](
monthlyFee: BigDecimal,
monthlyLimits: Limits,
excessCharge: BigDecimal
)
Subscriber ? Service ?
case class Subscriber( id, name, … )
trait BillingService
{
def checkServiceAccess(u:Subscriber,s:Service):Boolean
def rateServiceUsage(u:Subscriber,r:ServiceUsage):Subscriber
def ratePeriod(u:Subscriber, date: DateTime)
def acceptPayment(u:Subscriber, payment: Payment)
}
// Aggregate
// let’s add to Subscriber all fields, what needed.
adding fields for Subscriber aggregates.
case class Subscriber(id, name,
serviceInfos:Map[Service,SubscriberServiceInfo],
account: BigDecimal, …..
)
case class SubscriberServiceInfo[S<:Service,L<:S#Limits](
service: S, tariffPlan: tariffPlan[S,L], amountUsed:Double
)
trait BillingService
{
def checkServiceAccess(u:Subscriber,s:Service):Boolean =
u.serviceInfos(s).isDefined && u.account > 0
}
adding fields for Subscriber aggregates.
case class Subscriber(id, name,
serviceInfos:Map[Service,SubscriberServiceInfo[_,_]],
account: BigDecimal, …..
)
case class ServiceUsage(service, amount, when)
trait BillingService
{
def rateServiceUsage(u:Subscriber,r:ServiceUsage):Subscriber =
serviceInfo(r.service) match {
case Some(SubscriberServiceInfo(service,plan,amount)) =>
val price = ….
u.copy(account = u.account - price,
serviceInfo = serviceInfo.updated(s,
}
adding fields for Subscriber aggregates.
trait BillingService
{
def rateServiceUsage(u:Subscriber,r:ServiceUsage):Subscriber =
serviceInfo(r.service) match {
case Some(SubscriberServiceInfo(service,plan,amount)) =>
val price = ….
u.copy(account = u.account - price,
serviceInfo = serviceInfo.updated(s,
ServiceInfo(service,plan,amount+r.amount))
)
case None =>
throw new IllegalStateException(“service is not enabled”)
}
…………….
}
Subscriber aggregates [rate: lastPayedDate]
case class Subscriber(id, name,
serviceInfos:Map[Service,SubscriberServiceInfo[_,_]],
account: BigDecimal,
lastPayedDate: DateTime
)
trait BillingService
{
def ratePeriod(u:Subscriber,date:DateTime):Subscriber =
if (date < u.lastPayedDate) u
else {
val price = …..
subscriber.copy(account = u.account - price,
lastPayedDate = date+1.month)
}
}
Subscriber:
case class Subscriber(
id : Long,
name: String,
serviceInfos:Map[Service,SubscriberServiceInfo[_,_]],
account: BigDecimal,
lastPayedDate: DateTime
)
case class SubscriberServiceInfo[S<:Service,L<:S#Limits](
service: S,
tariffPlan: tariffPlan[L],
amountUsed:Double
)
Subscriber:
case class Subscriber(
id : Long,
name: String,
internetServiceInfo: ServiceInfo[Internet,Internet.Limits],
telephonyServiceInfo: ServiceInfo[Telephony,Telephony.Limits],
account: BigDecimal,
lastPayedDate: DateTime
) {
def serviceInfo(s:Service):SubscriberServiceInfo[s.type,s.Limits] =
….
def updateServiceInfo[S<:Service,L<:S#Limits](
serviceInfo:SubscriberServiceInfo[S,L]): Subscriber =
…
}
From domain model to implementation. [S1]
Subscriber
Service TariffPlan
Domain Data/ Aggregates Services
SubscriberOperations
TariffPlanOperations
….
Repository
DD — contains only essential data
Services — only functionality
Testable.
No mess with implementation.
Service calls — domain events
From domain model to implementation. [S1]
Improvements/Refactoring space:
• Errors handling
• Deaggregate
• Fluent DSL
Errors handling (design for failure)
trait BillingService
{
def checkServiceAccess(u:Subscriber,s:Service):Boolean
def rateServiceUsage(u:Subscriber,r:ServiceUsage):Subscriber
def ratePeriod(u:Subscriber, date: DateTime): Subscriber
def acceptPayment(u:Subscriber, payment:Payment):Subscriber
}
Design for failure:
trait BillingService
{
def checkServiceAccess(u:Subscriber,s:Service): Boolean
def rateServiceUsage(u:Subscriber,r:ServiceUsage):Try[Subscriber]
def ratePeriod(u:Subscriber, date: DateTime): Try[Subscriber]
def acceptPayment(u:Subscriber, payment:Payment): Subscriber
}
Design for failure:
sealed trait Try[+X]
case class Success[X](v:X) extends Try[X]
case class Failure(ex:Throwable) extends Try[Nothing]
when use Try / traditional exception handling?
Try — error recovery is a part of business layers.
(i.e. errors is domain-related)
Exception handling — error recovery is a part of infrastructure layer.
(i. e. errors is system-related)
Deaggregation:
trait Repository
{
def create[T](): T
def find[T](id: Id[T]): Try[T]
def save[T](obj: T): Try[Boolean]
}
Deaggregation:
trait Repository
{
def create[T](): T
def find[T](id: Id[T]): Try[T]
def save[T](obj: T) : Try[T]
………..
def subscriberServiceInfo[S<:Service,L<:S#Limits]
(id: Id[Subscriber], s:S): SubscriberServiceInfo[S,L]
def updateSubsriberServiceInfo[S<:Service,L<:S#Limits] (
id: Id[Subscriber],s:S,si:SubscriberServiceInfo[S,L]):
Try[SubscriberServiceInfo[S,L]]
}
Deaggregation:
trait BillingService
{
def checkServiceAccess(r:Repository, uid:Id[Subscriber],
s:Service): Boolean
def rateServiceUsage(r: Repository, uid:Id[Subscriber],
r:ServiceUsage):Try[Subscriber]
…..
}
Deaggregation:
trait BillingService
{
val repository: Repository
def checkServiceAccess(uid:Id[Subscriber], s:Service): Try[Boolean]
def rateServiceUsage(uid:Id[Subscriber], r:ServiceUsage):Try[Subscriber]
…..
}
// BillingService operations interpret repository
Deaggregation. [S2]
Subscriber
Service TariffPlan
Domain Data/ Aggregates Services
SubscriberOperations
TariffPlanOperations
….
Repository
Interpret
- Not for all cases
- Loss
- generality of repository
- simply logic
- Gain
- simple repository operations
- more efficient data access.
DSL: Domain Specific Language.
Idea: fluent syntax for fluent operations.
• Syntax sugar, can be used by non-programmers
• ‘Micro-interpreter/compiler’
• Internal/External
Let’s build simple Internal DSL for our tariff plans.
TariffPlan: DSL
case class TariffPlan[Limits](
monthlyFee: BigDecimal,
monthlyLimits: Limits,
excessCharge: BigDecimal
)
TariffPlan(100,Limits(1,100),2)
TariffPlan(montlyFee=100,
Internet.Limits(gb=1,bandwidth=100),
2)
100 hrn montly (1 gb) speed 100 excess 2 hrn per 1 gb
// let’s build
TariffPlan: DSL
(100 hrn) montly (1 gb) speed 100 excess (2 hrn) per (1 gb)
trait TariffPlanDSL[S <: Service, L <: S#Limits] {
implicit class ToHrn(v: Int)
{
def hrn = this
def monthly(x: LimitExpression) =
TariffPlan(v, x.limit, 0)
def per(x: QuantityExpression
}
1 hrn = 1.hrn =
new ToHrn(1).hrn
trait LimitExpression{
def limit: L
}
type QuantityExpression
{
def quantity: Int
}
TariffPlan: DSL
(100 hrn) montly (1 gb) speed 100 excess (2 hrn) per (1 gb)
object InternetTariffPlanDSL extends TariffPlanDSL[Internet.type, Internet.Limits]
implicit class Gb(v: Int) extends LimitExpression with QuantityExpression{
def gb = this
def limit = Internet.Limits(v,100)
def quantity = x
}
TariffPlan: DSL
(100 hrn) montly (1 gb) speed 100 excess (2 hrn) per (1 gb)
(100 hrn) montly (1 gb) == (100.hrn).montly(1.gb)
TariffPlan: DSL
(100 hrn) montly (1 gb) speed 100 excess ((2 hrn) per (1 gb))
trait TariffPlanDSL[S,L]
case class PerExpression(money: ToHrn, quantity: QuantityExpression)
trait RichTariffPlan(p: TariffPlan[L]) {
def excess(x: PerExpression) = p.copy(excessCost=x.v)
}
((100.hrn).montly(1.gb) speed 100).excess((2 hrn) per (1 gb))
TariffPlan: DSL
(100 hrn) montly (1 gb) speed 100 excess ((2 hrn) per (1 gb))
object InternetTariffPlanDSL[S,L]
trait RichTariffPlan(p: TariffPlan[L]) extends super.RichTariffPlan(p) {
def speed(x: Int) = p.copy(
monthlyLimits = p.monthlyLimits.copy(
bandwidth = x)
)
}
((100.hrn).montly(1.gb) speed 100).excess ((2.hrn).per(1.gb))
((100.hrn).montly(1.gb).speed(100)).excess ((2.hrn).per(1.gb))
DSL: (100 hrn) montly (1 gb) speed 100 excess ((2 hrn) per (1 gb))
Internal
External
Need some boilerplate code.
Useful when developers need fluent business
domain object notation.
Internally - combination of builder and interpreter patterns
Useful when external users (non-developers) want to describe
domain objects.
Internally - language-mini-interpreter.
// [scala default library include parser combinators]
Post-OOD domain modelling
Domain Data Objects
‘OO’ Objects with behavior
• Closed world.
• Different lifecycle can be described by
different types
• Composition over inheritance
Domain Services
• Open world.
• No data, only functionality. Calls can be replayed.
• Traditional inheritance
Infrastructure Services
• Interpreters of ‘domain services’ functions
// phantom types.
Thanks for attention.
Fully - implemented ‘tiny’ domain model and DSL:
https://github.com/rssh/scala-training-materials/tree/master/fwdays2015-examples/
Ruslan Shevchenko
Lynx Capital Partners
https://github.com/rssh
@rssh1

Behind OOD: domain modelling in post-OO world.

  • 1.
    Behind OOD // domainmodelling in a post-OO world Ruslan Shevchenko Lynx Capital Partners https://github.com/rssh @rssh1
  • 2.
    Domain modelling • Representationof business domain objects: • in ‘head’ of people • in code
  • 3.
    Domain modelling. • Outlinetypical OOD issues • Build • simple domain model for toy billing system. // scala, can be mapped to java. • internal DSL
  • 4.
    Domain modelling • TraditionalOO way: have layer of classes, which corresponds to domain objects. • Extensibility via inheritance in some consistent Universal Ontology • Intensional Equality [identity != attribute] • Object instance <=> Entity in real world
  • 5.
    Traditional OO WAY •Human is an upright, featherless biped with broad, flat nails.
  • 6.
    Traditional OO WAY •Human is an upright, featherless biped with broad, flat nails. Open for extensions close for modifications
  • 7.
    Traditional OO WAY •Human is an upright, featherless biped with broad, flat nails. Open for extensions close for modifications
  • 8.
    Traditional OO WAY •Intensional Equality [ mutability ] // same identity thought lifecycle
  • 9.
    Traditional OO WAY •Object in code <=> Object in real world class Person { int getId(); String getName(); int getAge(); Set<Person> getChilds() } — thread-safe ? — persistent updates ?
  • 10.
    Domain modelling • TraditionalOO way: have layer of classes, which corresponds to domain objects. • Extensibility via inheritance in some consistent Universal Ontology • Intensional Equality [identity != attribute] • Object instance <=> Entity in real world
  • 11.
    Domain modelling • TraditionalOO way: similar to early philosophy • Very general • Idealistic • Fit to concrete cases may be undefined
  • 12.
    Domain modelling • PostOO way: describe limited set of objects and relationships. • Algebra instead Ontology • Existential equality [identity == same attributes] • Rules of algebra <=> rules of reality.
  • 13.
    Domain modelling: whereto start. • Example: description of small billing system Subscriber Billing System Service PaymentPlanuse in accordance with Service is Internet | Telephony PaymentPlan is Monthly Fee for Quantity Limit and Overhead cost per Unit Unit Internet is Gbin Telephony is Minute and Bandwidth
  • 14.
    TariffPlan: Use limitand quantity from service. sealed trait Service { type Limits def quantity(l:Limits): BigDecimal } case object Internet extends Service { case class Limits(gb:Int,bandwidth:Int) def quantity(l:Limits) = l.gb } case object Telephony extends Service { type Limits = Int def quantity(l) = l
  • 15.
    TariffPlan: Price perlimit and charge case class TariffPlan[Limits]( monthlyFee: BigDecimal, monthlyLimits: Limits, excessCharge: BigDecimal )
  • 16.
    Subscriber ? Service? case class Subscriber( id, name, … ) trait BillingService { def checkServiceAccess(u:Subscriber,s:Service):Boolean def rateServiceUsage(u:Subscriber,r:ServiceUsage):Subscriber def ratePeriod(u:Subscriber, date: DateTime) def acceptPayment(u:Subscriber, payment: Payment) } // Aggregate // let’s add to Subscriber all fields, what needed.
  • 17.
    adding fields forSubscriber aggregates. case class Subscriber(id, name, serviceInfos:Map[Service,SubscriberServiceInfo], account: BigDecimal, ….. ) case class SubscriberServiceInfo[S<:Service,L<:S#Limits]( service: S, tariffPlan: tariffPlan[S,L], amountUsed:Double ) trait BillingService { def checkServiceAccess(u:Subscriber,s:Service):Boolean = u.serviceInfos(s).isDefined && u.account > 0 }
  • 18.
    adding fields forSubscriber aggregates. case class Subscriber(id, name, serviceInfos:Map[Service,SubscriberServiceInfo[_,_]], account: BigDecimal, ….. ) case class ServiceUsage(service, amount, when) trait BillingService { def rateServiceUsage(u:Subscriber,r:ServiceUsage):Subscriber = serviceInfo(r.service) match { case Some(SubscriberServiceInfo(service,plan,amount)) => val price = …. u.copy(account = u.account - price, serviceInfo = serviceInfo.updated(s, }
  • 19.
    adding fields forSubscriber aggregates. trait BillingService { def rateServiceUsage(u:Subscriber,r:ServiceUsage):Subscriber = serviceInfo(r.service) match { case Some(SubscriberServiceInfo(service,plan,amount)) => val price = …. u.copy(account = u.account - price, serviceInfo = serviceInfo.updated(s, ServiceInfo(service,plan,amount+r.amount)) ) case None => throw new IllegalStateException(“service is not enabled”) } ……………. }
  • 20.
    Subscriber aggregates [rate:lastPayedDate] case class Subscriber(id, name, serviceInfos:Map[Service,SubscriberServiceInfo[_,_]], account: BigDecimal, lastPayedDate: DateTime ) trait BillingService { def ratePeriod(u:Subscriber,date:DateTime):Subscriber = if (date < u.lastPayedDate) u else { val price = ….. subscriber.copy(account = u.account - price, lastPayedDate = date+1.month) } }
  • 21.
    Subscriber: case class Subscriber( id: Long, name: String, serviceInfos:Map[Service,SubscriberServiceInfo[_,_]], account: BigDecimal, lastPayedDate: DateTime ) case class SubscriberServiceInfo[S<:Service,L<:S#Limits]( service: S, tariffPlan: tariffPlan[L], amountUsed:Double )
  • 22.
    Subscriber: case class Subscriber( id: Long, name: String, internetServiceInfo: ServiceInfo[Internet,Internet.Limits], telephonyServiceInfo: ServiceInfo[Telephony,Telephony.Limits], account: BigDecimal, lastPayedDate: DateTime ) { def serviceInfo(s:Service):SubscriberServiceInfo[s.type,s.Limits] = …. def updateServiceInfo[S<:Service,L<:S#Limits]( serviceInfo:SubscriberServiceInfo[S,L]): Subscriber = … }
  • 23.
    From domain modelto implementation. [S1] Subscriber Service TariffPlan Domain Data/ Aggregates Services SubscriberOperations TariffPlanOperations …. Repository DD — contains only essential data Services — only functionality Testable. No mess with implementation. Service calls — domain events
  • 24.
    From domain modelto implementation. [S1] Improvements/Refactoring space: • Errors handling • Deaggregate • Fluent DSL
  • 25.
    Errors handling (designfor failure) trait BillingService { def checkServiceAccess(u:Subscriber,s:Service):Boolean def rateServiceUsage(u:Subscriber,r:ServiceUsage):Subscriber def ratePeriod(u:Subscriber, date: DateTime): Subscriber def acceptPayment(u:Subscriber, payment:Payment):Subscriber }
  • 26.
    Design for failure: traitBillingService { def checkServiceAccess(u:Subscriber,s:Service): Boolean def rateServiceUsage(u:Subscriber,r:ServiceUsage):Try[Subscriber] def ratePeriod(u:Subscriber, date: DateTime): Try[Subscriber] def acceptPayment(u:Subscriber, payment:Payment): Subscriber }
  • 27.
    Design for failure: sealedtrait Try[+X] case class Success[X](v:X) extends Try[X] case class Failure(ex:Throwable) extends Try[Nothing] when use Try / traditional exception handling? Try — error recovery is a part of business layers. (i.e. errors is domain-related) Exception handling — error recovery is a part of infrastructure layer. (i. e. errors is system-related)
  • 28.
    Deaggregation: trait Repository { def create[T]():T def find[T](id: Id[T]): Try[T] def save[T](obj: T): Try[Boolean] }
  • 29.
    Deaggregation: trait Repository { def create[T]():T def find[T](id: Id[T]): Try[T] def save[T](obj: T) : Try[T] ……….. def subscriberServiceInfo[S<:Service,L<:S#Limits] (id: Id[Subscriber], s:S): SubscriberServiceInfo[S,L] def updateSubsriberServiceInfo[S<:Service,L<:S#Limits] ( id: Id[Subscriber],s:S,si:SubscriberServiceInfo[S,L]): Try[SubscriberServiceInfo[S,L]] }
  • 30.
    Deaggregation: trait BillingService { def checkServiceAccess(r:Repository,uid:Id[Subscriber], s:Service): Boolean def rateServiceUsage(r: Repository, uid:Id[Subscriber], r:ServiceUsage):Try[Subscriber] ….. }
  • 31.
    Deaggregation: trait BillingService { val repository:Repository def checkServiceAccess(uid:Id[Subscriber], s:Service): Try[Boolean] def rateServiceUsage(uid:Id[Subscriber], r:ServiceUsage):Try[Subscriber] ….. } // BillingService operations interpret repository
  • 32.
    Deaggregation. [S2] Subscriber Service TariffPlan DomainData/ Aggregates Services SubscriberOperations TariffPlanOperations …. Repository Interpret - Not for all cases - Loss - generality of repository - simply logic - Gain - simple repository operations - more efficient data access.
  • 33.
    DSL: Domain SpecificLanguage. Idea: fluent syntax for fluent operations. • Syntax sugar, can be used by non-programmers • ‘Micro-interpreter/compiler’ • Internal/External Let’s build simple Internal DSL for our tariff plans.
  • 34.
    TariffPlan: DSL case classTariffPlan[Limits]( monthlyFee: BigDecimal, monthlyLimits: Limits, excessCharge: BigDecimal ) TariffPlan(100,Limits(1,100),2) TariffPlan(montlyFee=100, Internet.Limits(gb=1,bandwidth=100), 2) 100 hrn montly (1 gb) speed 100 excess 2 hrn per 1 gb // let’s build
  • 35.
    TariffPlan: DSL (100 hrn)montly (1 gb) speed 100 excess (2 hrn) per (1 gb) trait TariffPlanDSL[S <: Service, L <: S#Limits] { implicit class ToHrn(v: Int) { def hrn = this def monthly(x: LimitExpression) = TariffPlan(v, x.limit, 0) def per(x: QuantityExpression } 1 hrn = 1.hrn = new ToHrn(1).hrn trait LimitExpression{ def limit: L } type QuantityExpression { def quantity: Int }
  • 36.
    TariffPlan: DSL (100 hrn)montly (1 gb) speed 100 excess (2 hrn) per (1 gb) object InternetTariffPlanDSL extends TariffPlanDSL[Internet.type, Internet.Limits] implicit class Gb(v: Int) extends LimitExpression with QuantityExpression{ def gb = this def limit = Internet.Limits(v,100) def quantity = x }
  • 37.
    TariffPlan: DSL (100 hrn)montly (1 gb) speed 100 excess (2 hrn) per (1 gb) (100 hrn) montly (1 gb) == (100.hrn).montly(1.gb)
  • 38.
    TariffPlan: DSL (100 hrn)montly (1 gb) speed 100 excess ((2 hrn) per (1 gb)) trait TariffPlanDSL[S,L] case class PerExpression(money: ToHrn, quantity: QuantityExpression) trait RichTariffPlan(p: TariffPlan[L]) { def excess(x: PerExpression) = p.copy(excessCost=x.v) } ((100.hrn).montly(1.gb) speed 100).excess((2 hrn) per (1 gb))
  • 39.
    TariffPlan: DSL (100 hrn)montly (1 gb) speed 100 excess ((2 hrn) per (1 gb)) object InternetTariffPlanDSL[S,L] trait RichTariffPlan(p: TariffPlan[L]) extends super.RichTariffPlan(p) { def speed(x: Int) = p.copy( monthlyLimits = p.monthlyLimits.copy( bandwidth = x) ) } ((100.hrn).montly(1.gb) speed 100).excess ((2.hrn).per(1.gb)) ((100.hrn).montly(1.gb).speed(100)).excess ((2.hrn).per(1.gb))
  • 40.
    DSL: (100 hrn)montly (1 gb) speed 100 excess ((2 hrn) per (1 gb)) Internal External Need some boilerplate code. Useful when developers need fluent business domain object notation. Internally - combination of builder and interpreter patterns Useful when external users (non-developers) want to describe domain objects. Internally - language-mini-interpreter. // [scala default library include parser combinators]
  • 41.
    Post-OOD domain modelling DomainData Objects ‘OO’ Objects with behavior • Closed world. • Different lifecycle can be described by different types • Composition over inheritance Domain Services • Open world. • No data, only functionality. Calls can be replayed. • Traditional inheritance Infrastructure Services • Interpreters of ‘domain services’ functions // phantom types.
  • 42.
    Thanks for attention. Fully- implemented ‘tiny’ domain model and DSL: https://github.com/rssh/scala-training-materials/tree/master/fwdays2015-examples/ Ruslan Shevchenko Lynx Capital Partners https://github.com/rssh @rssh1