Flexible modelling
Magda Stożek
Keep your domain pure with Scala 3
The dream
The dream

The reality
The reality
Core logic polluted with
boilerplate/language internals
Infrastructure concerns
Steep learning curve
Scala 3
Lets you solve easy problems with easy tools
Extendable so that it's powerful for complex problems
Flexible so it lets you express what you want, where you want
Idea
User
---------------------------
id : Id
name : Name
email : Email
status : UserStatus
createdAt : Timestamp
---------------------------
UserStatus:
Active / Pending / SuspendedThe problem
package core
import search.SearchResult // 🚨
case class User(
id: Id, name: Name, email: Email, status: UserStatus, createdAt: Instant
) extends SearchResult { // 🚨
def searchLabel: String = s"${name.value} <${email.value}>" // 🚨
def toJson: String =
s"""{"id":"${id.value}","name":"${name.value}","status":"$status"}""" // 🚨
case class Name(value: String) extends AnyVal { // 🚨
def toAirlineSafe: String = value.map { // 🚨
case 'ą' | 'Ą' => 'a'
case 'ż' | 'Ż' | 'ź' | 'Ź' => 'z'
// (...)
}
}
}
sealed trait UserStatus // 🚨
object UserStatus {
case object Pending extends UserStatus
case object Active extends UserStatus
case class Suspended(reason: String) extends UserStatus
}Issue 1 - The Integration Leak
case class Name(value: String) extends AnyVal {
def toAirlineSafe: String = value.map {
case 'ą' | 'Ą' => 'a'
case 'ł' | 'Ł' => 'l'
// ...
case other => other
}
}makeReservation(name.toAirlineSafe)Usage
Workaround - Wrapper class
package airline
import core.Name
class AirlineSafeName(name: Name) {
def format: String = name.value.map {
case 'ą' | 'Ą' => 'a'
case 'ł' | 'Ł' => 'l'
case other => other
}
}val nameForAirline = new AirlineSafeName(user.name)
makeReservation(nameForAirline.toAirlineSafe)Usage
Workaround - Implicit class
package airline
import core.Name
object Formatters {
implicit class NameOps(val name: Name) extends AnyVal {
def toAirlineSafe: String = name.value.map {
case 'ą' | 'Ą' => 'a'
case 'ł' | 'Ł' => 'l'
case other => other
}
}
}import airline.Formatters._
user.name.toAirlineSafeUsage
The Fix - Extensions
package airline
import core.Name
extension (name: Name)
def toAirlineSafe: String = name.asString.map {
case 'ą' | 'Ą' => 'a'
case 'ł' | 'Ł' => 'l'
// ...
case other => other
}import airline.toAirlineSafe
user.name.toAirlineSafeUsage
The Fix - Extensions
package api
import core.User
extension (user: User)
def toJson: String =
s"""{"id":"${user.id.asString}","name":"${user.name.asString}"}"""
package search
import core.User
extension (user: User)
def searchLabel: String = s"${name.value} <${email.value}>"Issue 2: The performance leak
case class Name(value: String) extends AnyVal // 🚨printDiploma("usr_123") //❌ should not compileUsage:
The fix: Opaque types
opaque type Name = Stringval name = Name("Magda Stożek")
printDiploma(name)Usage
The fix: Opaque types
opaque type Name = Stringpackage core
val name = Name("Magda Stożek")
printDiploma(name)Usage
package diplomas
import core.Name
val name = Name("Magda Stożek") //❌ Not found: Name
printDiploma(name)But!
The Fix: Opaque types
opaque type Name = String
object Name:
def apply(value: String): Name = value
extension (name: Name) def asString: String = nameOpaque types - extended
opaque type Name = String
object Name:
def from(value: String): Option[Name] =
Option.when(value.trim.nonEmpty)(value)
extension (name: Name) def asString: String = nameIssue 3 - The Noise
sealed trait UserStatus
object UserStatus {
case object Pending extends UserStatus
case object Active extends UserStatus
case class Suspended(reason: String) extends UserStatus
}The fix - Enums
enum UserStatus:
case Pending, Active
case Suspended(reason: String)Enums - extended
enum UserStatus(val canAuthenticate: Boolean):
case Pending extends UserStatus(canAuthenticate = false)
case Active extends UserStatus(canAuthenticate = true)
case Suspended(reason: String) extends UserStatus(canAuthenticate = false)
def loginRejectionReason: Option[String] = this match
case Active => None
case Pending => Some("Account is awaiting email verification.")
case Suspended(r) => Some(s"Account suspended: $r")Act 4 - The API Leak (Before)
package core
import search.SearchResult // 🚨
case class User(
id: Id,
name: Name,
email: Email,
status: UserStatus,
createdAt: Instant
) extends SearchResult // 🚨
case class Department(name: String) extends SearchResult // 🚨 Act 4 - The Fix (Union Types)
package search
import core.{User, Department}
type SearchResult = User | Department
def search(query: String): List[SearchResult]The Pure Domain
package core
import java.time.Instant
case class User(id: Id, name: Name, email: Email, status: UserStatus, createdAt: Instant)
case class Department(name: String)
opaque type Id = String
opaque type Name = String
opaque type Email = String
enum UserStatus:
case Pending, Active
case Suspended(reason: String)The boundaries
package api
import core.User
extension (user: User)
def toJson: String =
s"""{"id":"${user.id.asString}","name":"${user.name.asString}"}"""
package search
import core.User
extension (user: User)
def searchLabel: String = s"${name.value} <${email.value}>"Domain extended
opaque type Name = String
object Name:
def from(value: String): Option[Name] =
Option.when(value.trim.nonEmpty)(value)
extension (name: Name) def asString: String = name
opaque type Email = String
object Email:
def from(value: String): Option[Email] =
Option.when(value.contains("@"))(value)
extension (email: Email) def asString: String = emailenum UserStatus(val canAuthenticate: Boolean):
case Pending extends UserStatus(canAuthenticate = false)
case Active extends UserStatus(canAuthenticate = true)
case Suspended(reason: String) extends UserStatus(canAuthenticate = false)
def loginRejectionReason: Option[String] = this match
case Active => None
case Pending => Some("Account is awaiting email verification.")
case Suspended(r) => Some(s"Account suspended: $r")Tools
AnyVal -> opaque types
implicit class -> extension method
sealed trait -> enum
sealed trait -> union types
Hand-written or AI?
Code quality is still our responsibility
Boundaries are key
Summary
Simple tools for simple problems
Extendable and powerful
Helps keep architecture boundaries
Thank you

Flexible modelling
By Magda Stożek
Flexible modelling
- 34
