Skip to content

Instantly share code, notes, and snippets.

@yannick-cw
Created August 13, 2017 17:39
Show Gist options
  • Save yannick-cw/5d877b7e7fc1a1c1b36027622165fd0c to your computer and use it in GitHub Desktop.
Save yannick-cw/5d877b7e7fc1a1c1b36027622165fd0c to your computer and use it in GitHub Desktop.
Complete example for a DDD implementation using Kleisli and EitherT with Future
import cats.data.{EitherT, Kleisli}
import common.{Amount, Valid}
import scala.concurrent.ExecutionContext.Implicits.global
import cats.implicits._
import scala.collection.mutable
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}
/**
* Domain Elements
*/
// entity
sealed trait Account {
def id: String
def balance: Balance
}
final case class SavingsAccount(id: String, balance: Balance) extends Account
object Account {
def updateBalance(a: Account, amount: Amount): Either[String, Account] =
if (a.balance.amount + amount >= 0) a match {
case s: SavingsAccount => Right(s.copy(balance = Balance(s.balance.amount + amount)))
} else Left(s"insufficient funds Account has ${a.balance.amount}, wanted $amount")
}
// value objects
case class Balance(amount: Amount)
object common {
type Amount = BigDecimal
type Valid[A] = EitherT[Future, String, A]
}
/** -------------------------------------------------
* Domain Services abstract
*/
trait AccountService[Account, Amount, Balance] {
type AccountOperation[A] = Kleisli[Valid, AccountRepository, A]
def open(balance: Balance): AccountOperation[Account]
def debit(no: String, amount: Amount): AccountOperation[Account]
def credit(no: String, amount: Amount): AccountOperation[Account]
def balance(no: String): AccountOperation[Balance]
def transfer(from: String, to: String, amount: Amount): AccountOperation[(Account, Account)] =
for {
a <- debit(from, amount)
b <- credit(to, amount)
} yield (a, b)
}
trait InterestCalculation {}
trait TaxCalculation {}
/**
* Implementation
*/
class AccountServiceInterpreter extends AccountService[Account, Amount, Balance] {
def open(balance: Balance): AccountOperation[Account] =
Kleisli(_.store(SavingsAccount("id", balance)))
def debit(no: String, amount: Amount): AccountOperation[Account] =
credit(no, -amount)
def credit(no: String, amount: Amount): AccountOperation[Account] = Kleisli { repo =>
(for {
a <- repo.query(no)
debitedA <- EitherT.fromEither[Future](Account.updateBalance(a, amount))
storedA <- repo.store(debitedA)
} yield storedA): Valid[Account]
}
def balance(no: String): AccountOperation[Balance] =
Kleisli(_.balance(no): Valid[Balance])
}
object AccountService extends AccountServiceInterpreter
/** -------------------------------------------------
* Repository
*/
trait AccountRepository {
def query(id: String): Valid[Account]
def store(a: Account): Valid[Account]
def balance(id: String): Valid[Balance] =
query(id).map(_.balance)
}
/**
* In Memory Implementation
*/
object InMemAccountRepo extends AccountRepository {
val mem: mutable.HashMap[String, Account] = mutable.HashMap[String, Account]()
def query(id: String): Valid[Account] =
EitherT(
Future.successful(
mem.get(id) match {
case None => Left("Could not find user")
case Some(a) => Right(a)
}
))
def store(a: Account): Valid[Account] =
EitherT(Future.successful {
mem.update(a.id, a)
Right(a): Either[String, Account]
})
}
/** -------------------------------------------------
* Usage
*/
object Run extends App {
import AccountService._
def op: AccountOperation[Balance] =
for {
a <- open(Balance(BigDecimal(99)))
credited <- credit(a.id, BigDecimal(22))
debited <- debit(credited.id, BigDecimal(100))
b <- balance(debited.id)
} yield b
val newBalance: Valid[Balance] = op.run(InMemAccountRepo)
println(Await.result(newBalance.value, Duration.Inf))
// Right(Balance(21))
val failed = debit("id", BigDecimal(22)).run(InMemAccountRepo)
println(Await.result(failed.value, Duration.Inf))
// Left(insufficient funds Account has 21, wanted -22)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment