val age: Int = 18
val name: String = "John Smith"
val cash: Double = 14.56models (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.56age == age // true
age == (age + 1) // false
name == "John Smith" // true
name == "John Doe" // false
cash == 14.56 // true
cash == 14.57 // falseval accountDetails: (Int, String, Double) = (age, name, cash)
accountDetails == accountDetails // true
accountDetails == accountDetails.copy(_1 = 19) // falsetype Age = Int
type Name = String
type Cash = Doubleval age: Age = 18
val name: Name = "John Smith"
val cash: Cash = 14.56case class AccountDetails(name: Name, age: Age, cash: Cash)val details = AccountDetails("John Smith", 18, 14.56)
details == details // true
details == details.copy(age = 19) // falsetype 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)) // falseAn information that something happened in the system.
case class UserCreationRequested(
data: UserData
)
case class UserCreated(
userId: UserId,
data: UserData,
at: Instant
)val createUser: UserCreationRequested => UserCreatedsealed 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 liabilityval 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 + version2import 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 // Unitprogram.unsafeRunAsync { result =>
println(s"Program finished with: $result")
} // Unitprogram.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 retiredval 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 versionsplanVersioningTest.unsafeRunSynccase 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 ContractEventcase 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 paymentimport fs2._
val contractEvents: Stream[IO, ContractEvent] = ...
val paymentsCreationProjection: IO[Unit] =
contractEvent
.collect {
case ContractCreated(contractId) => contractId
}
.evalMap(createPayment)
.drain.compileval fiber: IO[Fiber[IO, Unit]] =
paymentsCreationProjection.startval 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: