val age: Int = 18
val name: String = "John Smith"
val cash: Double = 14.56
models (data)
values
entities
events
behaviors
factories
repositories
services
An immutable object which is only distinguishable by its properties.
val age: Int = 18
val name: String = "John Smith"
val cash: Double = 14.56
age == age // true
age == (age + 1) // false
name == "John Smith" // true
name == "John Doe" // false
cash == 14.56 // true
cash == 14.57 // false
val accountDetails: (Int, String, Double) = (age, name, cash)
accountDetails == accountDetails // true
accountDetails == accountDetails.copy(_1 = 19) // false
type Age = Int
type Name = String
type Cash = Double
val age: Age = 18
val name: Name = "John Smith"
val cash: Cash = 14.56
case class AccountDetails(name: Name, age: Age, cash: Cash)
val details = AccountDetails("John Smith", 18, 14.56)
details == details // true
details == details.copy(age = 19) // false
type Name = String
type Surname = String
val name: Name = "John"
val surname: Surname = name // :(
case class Name(value: String) extends AnyVal
case class Surname(value: String) extends AnyVal
val name: Name = Name("John")
val surname: Surname = name // compiler error :)
case class Email(value: String) extends AnyVal
sealed trait EmailStatus
object EmailStatus {
case class Confirming(unconfirmed: Email)
extends EmailStatus
case class Confirmed(confirmed: Email)
extends EmailStatus
case class Changing(old: Email, newUnconfirmed: Email)
extends EmailStatus
}
val emailStatus: EmailStatus = ...
emailStatus match {
case EmailStatus.Confirming(unconfirmed) =>
println(s"Email $unconfirmed is not confirmed yet")
case EmailStatus.Confirmed(confirmed) =>
println(s"Email is confirmed $confirmed")
case EmailStatus.Changing(old, newUnconfirmed) =>
println(
s"Old email $old is confirmed" +
s" but user requested change $newUnconfirmed"
)
}
An object which has some intrinsic identity, which allows tracing its state’s changes in time.
case class UserId(value: UUID) extends AnyVal
case class Name(value: String) extends AnyVal
case class Surname(value: String) extends AnyVal
case class UserData(name: Name, surname: Surname)
case class User(id: UserId, data: UserData) {
override def equals(obj: Any): Boolean = obj match {
case User(otherId, _) => id == otherId
case _ => false
}
override def hashCode: Int = id.hashcode
}
val userId = UserId(UUID.randomUUID)
val data = UserData(Name("John"), Surname("Smith"))
val user = User(userId, data)
user == user // true
import com.softwaremill.quicklens._
user == user.modify(_.data.name).set(Name("Jane")) // true
user == user.copy(id = UserId(UUID.randomUUID)) // false
An information that something happened in the system.
case class UserCreationRequested(
data: UserData
)
case class UserCreated(
userId: UserId,
data: UserData,
at: Instant
)
val createUser: UserCreationRequested => UserCreated
sealed abstract case class PlanName(value: String) {
def +(another: PlanName): PlanName =
new PlanName(value + " " + another.value) {}
}
object PlanName {
def parse(value: String): Either[String, PlanName] =
if (value.trim.isEmpty) {
Left(s"'$value' is not a valid plan name")
} else Right(new PlanName(value.trim) {})
}
import cats.implicits._
val names: Either[String, (PlanName, PlanName)] =
(PlanName.parse("personal"), PlanName.parse("liability"))
.tupled
names match {
case Right((name1, name2)) =>
println(s"validation succeeded: ${plan1 + plan2}")
case Left(error) =>
println(error)
}
// validation succeeded: personal liability
val version2: PlanName = ... // PlanName("version 2")
PlanName.parse("household").flatMap { name1 =>
PlanName.parse(name1.value + " insurance").map { name2 =>
name2 + version2
}
}
// Right(PlanName("household insurance version 2"))
for {
name1 <- PlanName.parse("household")
name2 <- PlanName.parse(name1.value + " insurance")
} yield name2 + version2
import cats.effect._
val program = IO.delay(scala.io.StdIn.readLine) // IO[String]
.map(PlanName.parse) // IO[Either[String, PlanName]]
.flatMap {
case Left(error) => IO.raiseError(new Exception(error))
case Right(name) => IO.pure(name)
} // IO[PlanName]
.map { name =>
println(s"Valid plan name: $name")
} // IO[Unit]
.handleError {
case e: Throwable => e.printStackTrace()
} // IO[Unit]
program.unsafeRunSync // Unit
program.unsafeRunAsync { result =>
println(s"Program finished with: $result")
} // Unit
program.unsafeToFuture // Future[Unit]
import scala.concurrent.duration._
IO.delay(scala.io.StdIn.readLine)
.flatMap { in =>
val nap = IO.sleep(2.second)
val asBool = nap >> IO.delay(in.toBoolean).attempt
val asInt = nap >> IO.delay(in.toInt).attempt
val asDouble = nap >> IO.delay(in.toDouble).attempt
(asBool, asInt, asDouble).parMapN { (b, i, d) =>
println(s"$in:\nbool: $b,\nint: $i,\ndouble: $d")
}
}
.unsafeRunAsync(result => println(result))
case class PlanId(value: UUID) extends AnyVal
case class PlanVersion(value: Int) extends AnyVal
case class PlanVersionedId(id: PlanId, version: PlanVersion)
sealed trait PlanStatus
object PlanStatus {
case object NotLaunched extends PlanStatus
case class Launched(at: Instant) extends PlanStatus
case class Retired(validFrom: Instant, validUntil: Instant)
extends PlanStatus
}
case class PlanData(name: PlanName, status: PlanStatus)
case class Plan(versionedId: PlanVersionedId, data: PlanData){
override def equals(obj: Any): Boolean = obj match {
case Plan(`versionedId`, _) => true
case _ => false
}
override def hashCode: Int = versionedId.hashCode
}
trait PlanServices {
def createPlan(data: PlanData): IO[Plan]
def updatePlan(id: PlanId, data: PlanData): IO[Plan]
def launchPlan(versionedId: PlanVersionedId,
at: Instant): IO[Plan]
def retirePlan(versionedId: PlanVersionedId,
at: Instant): IO[Plan]
}
trait PlanRepository {
def listActivePlans: IO[List[Plan]]
def listPlanVersions(id: PlanId): IO[List[Plan]]
def getExactPlan(versionedId: PlanVersionedId): IO[Plan]
def getLastestPlan(id: PlanId): IO[Plan]
}
val planName = PlanName.parse("my plan").right.get
val planLifecycleTest = for {
plan1 <- planServices.createPlan(
PlanData(planName, PlanStatus.NotLaunched))
_ <- planServices.launchPlan(plan1.versionedId, Instant.now)
_ <- IO.sleep(1.second)
active <- planRepository.listActivePlans
_ = println("Active plans\n" + active.mkString("\n"))
retired <- planServices.retirePlan(plan1.versionedId,
Instant.now)
} yield retired
val planVersioningTest = for {
oldPlan <- planLifecycleTest
plan2 <- planServices.updatePlan(
oldPlan.versionedId.id,
PlanData(oldPlan.data.name, PlanStatus.NotLaunched))
versions <- planRepository.listPlanVersions(
plan2.versionedId.id)
_ = println("Versions\n" + versions.mkString("\n"))
} yield versions
planVersioningTest.unsafeRunSync
case class CustomerId(value: UUID) extends AnyVal
case class ContractId(value: UUID) extends AnyVal
sealed trait ContractDuration
object ContractDuration {
case class Fixed(from: ZonedDate, until: Instant)
extends ContractDuration
case class Renewed(from: Instant, lastRenewal: Instant,
terminated: Option[Instant])
extends ContractDuration
}
case class Coverage(start: Instant, end: Instant)
case class ContractData(
customerId: CustomerId, planVersion: PlanVersionedId
duration: ContractDuration, coverage: Coverage
)
case class Contract(id: ContractId, data: ContractData) {
override def equals(obj: Any): Boolean = obj match {
case Plan(`id`, _) => true
case _ => false
}
override def hashCode: Int = versionedId.hashCode
}
trait ContractServices {
def createContract(data: ContractData): IO[Contract]
def renewContract(
id: ContractId,
nextRenewal: Instant,
nextCoverageEnd: Instant): IO[List[Contract]]
def terminateContract(id: ContractId,
at: Instant): IO[Contract]
}
trait ContractRepository {
def getContract(id: ContractId): IO[Contract]
def listContractsForCustomer(
customerId: CustomerId): IO[List[Contract]]
def listContractsForRenewal(at: Instant): IO[List[Contract]]
}
sealed trait ContractEvent
case class ContractCreated(id: ContractId)
extends ContractEvent
case class ContractRenewed(id: ContractId)
extends ContractEvent
case class ContractTerminated(id: ContractId)
extends ContractEvent
case class CustomerData(
name: CustomerName,
surname: CustomerSurname,
addresses: NonEmptyList[Address],
email: Email
)
case class Customer(id: CustomerId, data: CustomerData) {
// ... override equals and hashCode
}
trait CustomerRepository {
def getCustomerById(id: CustomerId): IO[Customer]
}
case class PaymentId(value: UUID) extends AnyVal
sealed trait PaymentType
object PaymentType {
case object SinglePayment extends PaymentType
case class Subscription(renewEvery: Duration)
extends PaymentType
}
sealed trait PaymentStatus
object PaymentStatus {
case object Scheduled extends PaymentStatus
case object Completed extends PaymentStatus
case object Failed extends PaymentStatus
case object Cancelled extends PaymentStatus
}
sealed trait PaymentMethod
object PaymentMethod {
case class CreditCard(...) extends PaymentMethod
case class Stripe(...) extends PaymentMethod
case class PayPal(...) extends PaymentMethod
...
}
case class PaymentData(
contractId: ContractId,
paymentType: PaymentType,
paymentMethod: PaymentMethod,
status: PaymentStatus,
amount: Money,
customerId: CustomerId,
customerName: CustomerName,
customerSurname: CustomerSurname,
invoiceAddress: Address
)
case class Payment(id: PaymentId, data: PaymentData) {
// ... override equals and hashCode
}
import squants.market._
trait QuotingServices {
def quoteForContract(
data: ContractData): IO[(Money, PaymentType)]
}
trait CustomerPaymentMethodServices {
def setMethodForCustomer(
customerId: CustomerId,
paymentMethod: PaymentMethod): IO[Unit]
def getMethodForCustomer(
customerId: CustomerId): IO[PaymentMethod]
}
trait PaymentServices {
def schedulePayment(data: PaymentData): IO[Payment]
def renewPayment(paymentId: PaymentId,
at: Instant): IO[Payment]
def retryPayment(
paymentId: PaymentId,
newMethod: Option[PaymentMethod]): IO[Payment]
}
def createPayment(contractId: ContractId) = for {
contract <- contractRepository.getContract(contractId)
customerId = contractData.customerId
customer <- customerRepository.getCustomerById(customerId)
method <- customerPaymentMethodServices
.getMethodForCustomer(customerId)
(price, paymentType) <-
quotingServices.quoteForContract(contract.data)
data = PaymentData(contractId, paymentType, method,
PaymentStatus.Scheduled, price, customerId,
customerData.name, customerData.surname,
customerData.addresses.head)
payment <- paymentServices.createPayment(data)
} yield payment
import fs2._
val contractEvents: Stream[IO, ContractEvent] = ...
val paymentsCreationProjection: IO[Unit] =
contractEvent
.collect {
case ContractCreated(contractId) => contractId
}
.evalMap(createPayment)
.drain.compile
val fiber: IO[Fiber[IO, Unit]] =
paymentsCreationProjection.start
val projections = for {
_ <- paymentsCreationProjection.start
_ <- paymentsRenewedProjection.start
_ <- paymentsCancelledProjection.start
_ = println("All projections started")
} yield ()
resources (e.g. cats.effect.Resource[IO, A
])
persistence (e.g. Doobie)
serialization (e.g. Circe)
That I’ve written: