+ All Categories
Home > Software > Functional Programming & Event Sourcing - a pair made in heaven

Functional Programming & Event Sourcing - a pair made in heaven

Date post: 22-Jan-2017
Category:
Upload: pawel-szulc
View: 674 times
Download: 0 times
Share this document with a friend
194
Functional Programming & Event Sourcing A pair made in heaven twitter: @rabbitonweb, email: [email protected]
Transcript

Functional Programming & Event Sourcing

A pair made in heaven

twitter: @rabbitonweb, email: [email protected]

Functional Programming

● no assignment statements

● no assignment statements● no variables

● no assignment statements● no variables ● once given a value, never

change

● no assignment statements● no variables ● once given a value, never

change● no side-effects at all

● no assignment statements● no variables ● once given a value, never

change● no side-effects at all

“The functional programmer sounds rather like a mediæval monk,

“The functional programmer sounds rather like a mediæval monk, denying himself the pleasures of life

“The functional programmer sounds rather like a mediæval monk, denying himself the pleasures of life in the hope that it will make him virtuous.”

Functional Programming

case class User(id: Long, fn: String, ln: String)

case class User(id: Long, fn: String, ln: String)

class Cache { def check(id: Long): Option[User] = ??? }

case class User(id: Long, fn: String, ln: String)

class Cache { def check(id: Long): Option[User] = ??? }

case class UserRepo(cache: Cache) {

def retrieve(id: Long): User = ???

}

case class User(id: Long, fn: String, ln: String)

class Cache { def check(id: Long): Option[User] = ??? }

case class UserRepo(cache: Cache) {

def retrieve(id: Long): User = ???

}

class UserFinder(cache: Cache, repo: UserRepo) {

}

case class User(id: Long, fn: String, ln: String)

class Cache { def check(id: Long): Option[User] = ??? }

case class UserRepo(cache: Cache) {

def retrieve(id: Long): User = ???

}

class UserFinder(cache: Cache, repo: UserRepo) {

def findUser(id: Long): User = {

}

}

case class User(id: Long, fn: String, ln: String)

class Cache { def check(id: Long): Option[User] = ??? }

case class UserRepo(cache: Cache) {

def retrieve(id: Long): User = ???

}

class UserFinder(cache: Cache, repo: UserRepo) {

def findUser(id: Long): User = {

val maybeUser: Option[User] = cache.check(id)

}

}

case class User(id: Long, fn: String, ln: String)

class Cache { def check(id: Long): Option[User] = ??? }

case class UserRepo(cache: Cache) {

def retrieve(id: Long): User = ???

}

class UserFinder(cache: Cache, repo: UserRepo) {

def findUser(id: Long): User = {

val maybeUser: Option[User] = cache.check(id)

if (maybeUser.isDefined) {

maybeUser.get

}

}

}

case class User(id: Long, fn: String, ln: String)

class Cache { def check(id: Long): Option[User] = ??? }

case class UserRepo(cache: Cache) {

def retrieve(id: Long): User = ???

}

class UserFinder(cache: Cache, repo: UserRepo) {

def findUser(id: Long): User = {

val maybeUser: Option[User] = cache.check(id)

if (maybeUser.isDefined) {

maybeUser.get

} else {

val user: User = repo.retrieve(id)

cache.insert(user.id, user)

}

}

}

No concept of ‘time’

f(input): output

input f(input): output

input f(input): output output

input f(input): output output g(input): output

input f(input): output output g(input): output output

input f(input): output output g(input): output output

h = g o f

input f(input): output output g(input): output output

Modularity & composition

h = g o f

input f(input): output output g(input): output output

/** * This function returns a reversed list * @param list A list to be reversed * @return A reversed list */public List<T> reverse(List<t> list) { ??? }

/** * This function returns a reversed list * @param list A list to be reversed * @return A reversed list */public List<T> reverse(List<t> list) { ??? }

/** * This function returns a reversed list * @param list A list to be reversedpublic List<T> reverse(List<t> list) { ??? }

/** * This function returns a reversed listpublic List<T> reverse(List<t> list) { ??? }

/**public List<T> reverse(List<t> list) { ??? }

public List<T> reverse(List<t> list) { ??? }

public List<T> reverse(List<t> list) { return list.sort();}

def smdfknmsdfp[A](a: A): A = ???

def smdfknmsdfp[A](a: A): A = a

def identity[A](a: A): A = a

def smdfknmsdfp[A](a: A): A = a

def smdfknmsdfp(a: Int): Int = ???

def smdfknmsdfp(a: Int): Int = a = a + 10 = 10

“Why Functional Programming Matters”J. Hughes

http://comjnl.oxfordjournals.org/content/32/2/98.full.pdf

“Why Functional Programming Matters”J. Hughes, Nov. 1988

http://comjnl.oxfordjournals.org/content/32/2/98.full.pdf

Soft introduction

“Why Functional Programming Matters”J. Hughes, Nov. 1988

http://comjnl.oxfordjournals.org/content/32/2/98.full.pdf

“Program Design by Calculation”J.N. Oliveira

http://www4.di.uminho.pt/~jno/ps/pdbc_part.pdf

“Program Design by Calculation”J.N. Oliveira, Draft

http://www4.di.uminho.pt/~jno/ps/pdbc_part.pdf

Patterns in FP World

“Program Design by Calculation”J.N. Oliveira, Draft

http://www4.di.uminho.pt/~jno/ps/pdbc_part.pdf

Patterns in FP World Math Matters

“Program Design by Calculation”J.N. Oliveira, Draft

http://www4.di.uminho.pt/~jno/ps/pdbc_part.pdf

Functional Programming

Functional Programming

How to do something useful?

How to do something useful?

case class User(id: Long, fn: String, ln: String)

class Cache { def check(id: Long): Option[User] = ??? }

case class UserRepo(cache: Cache) {

def retrieve(id: Long): User = ???

}

class UserFinder(cache: Cache, repo: UserRepo) {

def findUser(id: Long): User = {

val maybeUser: Option[User] = cache.check(id)

if (maybeUser.isDefined) {

maybeUser.get

} else {

val user: User = repo.retrieve(id)

cache.insert(user.id, user)

}

}

}

case class User(id: Long, fn: String, ln: String)

case class User(id: Long, fn: String, ln: String)

class Cache {}

case class User(id: Long, fn: String, ln: String)

class Cache {}

def check(id: Long)(cache: Cache): (Cache, Option[User]) = ...

case class User(id: Long, fn: String, ln: String)

class Cache {}

def check(id: Long)(cache: Cache): (Cache, Option[User]) = ...

def retrieve(id: Long)(cache: Cache): (Cache, User) = ...

case class User(id: Long, fn: String, ln: String)

class Cache {}

def check(id: Long)(cache: Cache): (Cache, Option[User]) = ...

def retrieve(id: Long)(cache: Cache): (Cache, User) = ...

def findUser(id: Long)(cache: Cache): (Cache, User) = {

}

}

case class User(id: Long, fn: String, ln: String)

class Cache {}

def check(id: Long)(cache: Cache): (Cache, Option[User]) = ...

def retrieve(id: Long)(cache: Cache): (Cache, User) = ...

def findUser(id: Long)(cache: Cache): (Cache, User) = {

val (c, mu) = check(id)(cache)

}

}

case class User(id: Long, fn: String, ln: String)

class Cache {}

def check(id: Long)(cache: Cache): (Cache, Option[User]) = ...

def retrieve(id: Long)(cache: Cache): (Cache, User) = ...

def findUser(id: Long)(cache: Cache): (Cache, User) = {

val (c, mu) = check(id)(cache)

mu match {

case Some(u) => (c, u)

}

}

}

case class User(id: Long, fn: String, ln: String)

class Cache {}

def check(id: Long)(cache: Cache): (Cache, Option[User]) = ...

def retrieve(id: Long)(cache: Cache): (Cache, User) = ...

def findUser(id: Long)(cache: Cache): (Cache, User) = {

val (c, mu) = check(id)(cache)

mu match {

case Some(u) => (c, u)

case None => retrieve(id)(c)

}

}

}

case class User(id: Long, fn: String, ln: String)

class Cache {}

def check(id: Long)(cache: Cache): (Cache, Option[User]) = ...

def retrieve(id: Long)(cache: Cache): (Cache, User) = ...

def findUser(id: Long)(cache: Cache): (Cache, User) = {

val (c, mu) = check(id)(cache)

mu match {

case Some(u) => (c, u)

case None => retrieve(id)(c)

}

}

}

S => (S, A)

State[S, A]S => (S, A)

State[S, A]S => (S, A)

.run(S)

State[S, A]S => (S, A)

.map(A => B): State[S, B]

State[S, A]S => (S, A)

.flatMap(A => State[S, B]): State[S,B]

object State {

def apply[S, A] (f: S => (S,A)): State[S, A] =

}

object State {

def apply[S, A] (f: S => (S,A)): State[S, A] =

new State[S, A] {

def run(s: S) = f(s)

}

}

object State {

def apply[S, A] (f: S => (S,A)): State[S, A] =

new State[S, A] {

def run(s: S) = f(s)

}

}

def check(id: String) =

object State {

def apply[S, A] (f: S => (S,A)): State[S, A] =

new State[S, A] {

def run(s: S) = f(s)

}

}

def check(id: String) =

(c: Cache) => (c, c.get(id))

object State {

def apply[S, A] (f: S => (S,A)): State[S, A] =

new State[S, A] {

def run(s: S) = f(s)

}

}

def check(id: String) = State[Cache, Option[User]].apply {

(c: Cache) => (c, c.get(id))

}

object State {

def apply[S, A] (f: S => (S,A)): State[S, A] =

new State[S, A] {

def run(s: S) = f(s(

}

}

def check(id: String) = State[Cache, Option[User]]{

(c: Cache) => (c, c.get(id))

}

trait State[S, +A] {

}

trait State[S, +A] {

def run(initial: S): (S, A)

}

trait State[S, +A] {

def run(initial: S): (S, A)

def map[B](f: A => B): State[S, B] =

}

}

trait State[S, +A] {

def run(initial: S): (S, A)

def map[B](f: A => B): State[S, B] =

State {

}

}

}

trait State[S, +A] {

def run(initial: S): (S, A)

def map[B](f: A => B): State[S, B] =

State { s0 =>

(_, _ )

}

}

}

trait State[S, +A] {

def run(initial: S): (S, A)

def map[B](f: A => B): State[S, B] =

State { s0 =>

(_, f(a))

}

}

}

trait State[S, +A] {

def run(initial: S): (S, A)

def map[B](f: A => B): State[S, B] =

State { s0 =>

val (s, a) = run(s0)

(_, f(a))

}

}

}

trait State[S, +A] {

def run(initial: S): (S, A)

def map[B](f: A => B): State[S, B] =

State { s0 =>

val (s, a) = run(s0)

(s, f(a))

}

}

}

trait State[S, +A] {

def run(initial: S): (S, A)

def map[B](f: A => B): State[S, B] =

State { s0 =>

val (s, a) = run(s0)

(s, f(a))

}

}

def flatMap[B](f: A => State[S,B]): State[S, B] =

}

}

trait State[S, +A] {

def run(initial: S): (S, A)

def map[B](f: A => B): State[S, B] =

State { s0 =>

val (s, a) = run(s0)

(s, f(a))

}

}

def flatMap[B](f: A => State[S,B]): State[S, B] =

f(a)

}

}

trait State[S, +A] {

def run(initial: S): (S, A)

def map[B](f: A => B): State[S, B] =

State { s0 =>

val (s, a) = run(s0)

(s, f(a))

}

}

def flatMap[B](f: A => State[S,B]): State[S, B] =

val (s, a) = run(s0)

f(a)

}

}

trait State[S, +A] {

def run(initial: S): (S, A)

def map[B](f: A => B): State[S, B] =

State { s0 =>

val (s, a) = run(s0)

(s, f(a))

}

}

def flatMap[B](f: A => State[S,B]): State[S, B] =

State { s0 =>

val (s, a) = run(s0)

f(a)

}

}

}

trait State[S, +A] {

def run(initial: S): (S, A)

def map[B](f: A => B): State[S, B] =

State { s0 =>

val (s, a) = run(s0)

(s, f(a))

}

}

def flatMap[B](f: A => State[S,B]): State[S, B] =

State { s0 =>

val (s, a) = run(s0)

f(a).run(s)

}

}

}

case class User(id: Long, fn: String, ln: String)

class Cache {}

def check(id: Long)(cache: Cache): (Cache, Option[User]) = ???

def retrieve(id: Long)(cache: Cache): (Cache, User) = ???

def findUser(id: Long)(cache: Cache): (Cache, User) = {

val (c, mu) = check(id)(cache)

mu match {

case Some(u) => (c, u)

case None => retrieve(id)(c)

}

}

}

case class User(id: Long, fn: String, ln: String)

class Cache {}

def check(id: Long)(cache: Cache): (Cache, Option[User]) = ???

def retrieve(id: Long)(cache: Cache): (Cache, User) = ???

def findUser(id: Long)(cache: Cache): (Cache, User) = {

val (c, mu) = check(id)(cache)

mu match {

case Some(u) => (c, u)

case None => retrieve(id)(c)

}

}

}

case class User(id: Long, fn: String, ln: String)

class Cache {}

def check(id: Long): State[Cache, Option[User]] = ???

def retrieve(id: Long): State[Cache, User] = ???

def findUser(id: Long): State[Cache, User] = {

for {

maybeUser <- check(id)

user <- maybeUser match {

case Some(u) => State { c => (c, u)}

case None => retrieve(id)

}

} yield (user)

}

Event Sourcing

Event Sourcing driven by business

Bloggers Conf App

Bloggers Conf App

● Can create an account

Bloggers Conf App

● Can create an account● List all bloggers already using the app

Bloggers Conf App

● Can create an account● List all bloggers already using the app● Mark/unmark other blogger as a friend

Bloggers Conf App

● Can create an account● List all bloggers already using the app● Mark/unmark other blogger as a friend● Mark/unmark other blogger as an enemy

Bloggers Conf App

● Can create an account● List all bloggers already using the app● Mark/unmark other blogger as a friend● Mark/unmark other blogger as an enemy● Deactivate its account

Bloggers Conf App

Bloggers

id first_name last_name active

1 Jan Kowalski T

2 Krystian Nowak T

3 Malgorzata Kucharska T

Bloggers Conf App

Bloggers

id first_name last_name active

1 Jan Kowalski T

2 Krystian Nowak T

3 Malgorzata Kucharska T

Friends

id friend_id

3 1

Bloggers Conf App

Bloggers

id first_name last_name active

1 Jan Kowalski T

2 Krystian Nowak T

3 Malgorzata Kucharska T

Friends

id friend_id

3 1

Enemies

id enemy_id

3 2

Bloggers Conf App

Bloggers

id first_name last_name active

1 Jan Kowalski T

2 Krystian Nowak T

3 Malgorzata Kucharska T

Friends

id friend_id

3 1

Enemies

id enemy_id

3 2

The Structure

Structure is not that important

Structure is not that important

Changes more often than behaviour

Start thinking about facts occurring

Start thinking about facts occurring

Derive structure from them

Blogger Account Created (id=3)

Blogger Account Created (id=3)

Befriended Blogger id=1

Blogger Account Created (id=3)

Befriended Blogger id=1

Made Enemy of Blogger id=2

Blogger Account Created (id=3)

Befriended Blogger id=1

Made Enemy of Blogger id=2

Bloggers

id first_name last_name active

1 Jan Kowalski T

2 Krystian Nowak T

Friends

id friend_id

Enemies

id enemy_id

Blogger Account Created (id=3)

Befriended Blogger id=1

Made Enemy of Blogger id=2

Bloggers

id first_name last_name active

1 Jan Kowalski T

2 Krystian Nowak T

3 Malgorzata Kucharska T

Friends

id friend_id

Enemies

id enemy_id

Blogger Account Created (id=3)

Befriended Blogger id=1

Made Enemy of Blogger id=2

Bloggers

id first_name last_name active

1 Jan Kowalski T

2 Krystian Nowak T

3 Malgorzata Kucharska T

Friends

id friend_id

3 1

Enemies

id enemy_id

Blogger Account Created (id=3)

Befriended Blogger id=1

Made Enemy of Blogger id=2

Bloggers

id first_name last_name active

1 Jan Kowalski T

2 Krystian Nowak T

3 Malgorzata Kucharska T

Friends

id friend_id

3 1

Enemies

id enemy_id

3 2

Blogger Account Created (id=3)

Befriended Blogger id=1

Made Enemy of Blogger id=2

Blogger Account Created (id=3)

Befriended Blogger id=1

Made Enemy of Blogger id=2

Befriended Blogger id=2

Unfriended Blogger id=2

Blogger Account Created (id=3)

Befriended Blogger id=1

Made Enemy of Blogger id=2

Event Sourcing driven by business

Event Sourcing The only model that does not lose data

“Enemy of my enemy is my friend”

Bloggers

id first_name last_name active

1 Jan Kowalski T

2 Krystian Nowak T

3 Malgorzata Kucharska T

Friends

id friend_id

3 1

Enemies

id enemy_id

3 2

Bloggers

id first_name last_name active

1 Jan Kowalski T

2 Krystian Nowak T

3 Malgorzata Kucharska T

4 Tomasz Młynarski T

Friends

id friend_id

3 1

Enemies

id enemy_id

3 2

4 2

Bloggers

id first_name last_name active

1 Jan Kowalski T

2 Krystian Nowak T

3 Malgorzata Kucharska T

4 Tomasz Młynarski T

5 Monika Jagoda T

Friends

id friend_id

3 1

Enemies

id enemy_id

3 2

4 2

2 5

id = 3

id = 2

id = 1

id = 4

id = 5

id = 6

id = 3

id = 2

id = 1

id = 4

id = 5

id = 6

id = 3

id = 2

id = 1

id = 4

id = 5

id = 6

id = 3

id = 2

id = 1

id = 4

id = 5

id = 6

id = 3

id = 2

id = 1

id = 4

id = 5

id = 6

id = 3

id = 2

id = 1

id = 4

id = 5

id = 6

id = 3

id = 2

id = 1

id = 4

id = 5

id = 6

id = 3

id = 2

id = 1

id = 4

id = 5

id = 6

Benefits of Event Sourcing

Benefits of Event Sourcing● Ability to go in time and figure out exactly what have

happened

Benefits of Event Sourcing● Ability to go in time and figure out exactly what have

happened● Scientific measurements over time, compare time

periods

Benefits of Event Sourcing● Ability to go in time and figure out exactly what have

happened● Scientific measurements over time, compare time

periods● Built-in audit log

Benefits of Event Sourcing● Ability to go in time and figure out exactly what have

happened● Scientific measurements over time, compare time

periods● Built-in audit log● Enables temporal querying

Benefits of Event Sourcing● Ability to go in time and figure out exactly what have

happened● Scientific measurements over time, compare time

periods● Built-in audit log● Enables temporal querying● Fits well with machine learning

Benefits of Event Sourcing● Ability to go in time and figure out exactly what have

happened● Scientific measurements over time, compare time

periods● Built-in audit log● Enables temporal querying● Fits well with machine learning● Preserves history - question not yet asked

Benefits of Event Sourcing● Ability to go in time and figure out exactly what have

happened● Scientific measurements over time, compare time

periods● Built-in audit log● Enables temporal querying● Fits well with machine learning● Preserves history - question not yet asked● Writing regression tests is easy

Benefits of Event Sourcing● Ability to go in time and figure out exactly what have

happened● Scientific measurements over time, compare time

periods● Built-in audit log● Enables temporal querying● Fits well with machine learning● Preserves history - question not yet asked● Writing regression tests is easy● Polyglot data

Drawbacks of Event Sourcing

Drawbacks of Event Sourcing● Historical record of your bad decisions

Drawbacks of Event Sourcing● Historical record of your bad decisions● Handling event duplicates

Drawbacks of Event Sourcing● Historical record of your bad decisions● Handling event duplicates● Data eventually consistent

How to implement it?

Events vs Commands

Journal

Journal

Journal

val id = “”val firstName: String = “”val lastName: String = “”val friends: List[String] = List()

Journal

val id = “”val firstName: String = “”val lastName: String = “”val friends: List[String] = List()

Journal

val id = “”val firstName: String = “”val lastName: String = “”val friends: List[String] = List()

Journal

val id = “”val firstName: String = “”val lastName: String = “”val friends: List[String] = List()

Initialized(“1”, “Jan”, “Kowalski”

Journal

val id = “”val firstName: String = “”val lastName: String = “”val friends: List[String] = List()

Initialized(“1”, “Jan”, “Kowalski”

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List()

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List()

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List()

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List()

Befriended(“10”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List()

Befriended(“10”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”)

Befriend(“31”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”)

Befriend(“31”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”)

Befriend(“31”)

validation

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”)

Befriend(“31”)

validation

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”)

Befriend(“34”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”)

Befriend(“34”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”)

Befriend(“34”)

validation

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”)

Befriend(“34”)

validation

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”)

Befriend(“34”)

Befriended(“34”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”)

Befriend(“34”)

Befriended(“34”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”)

Befriend(“34”)

Befriended(“34”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”)

Befriend(“34”)

Befriended(“34”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”)

Befriend(“34”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”)

Befriend(“34”)

ACK

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”, “34”)

Befriend(“34”)

Journal

val id = “1”val firstName: String = “Jan”val lastName: String = “Kowalski”val friends: List[String] = List(“10”, “31”, “34”)

Let’s see some code!https://github.

com/rabbitonweb/es_cqrs_example

What we are missing?

What we are missing?

1. Read-model

What we are missing?

1. Read-model2. Validation

And that’s all folks!

Paweł Szulc

Paweł Szulchttp://rabbitonweb.com

Paweł Szulchttp://rabbitonweb.com

@rabbitonweb

Thank you!


Recommended