scalaintermediate

Type-Safe Identifiers Pattern

Create type-safe ID wrappers to prevent mixing different entity IDs at compile time.

scala
import java.util.UUID

// Generic tagged type for IDs
trait IdTag
trait UserIdTag extends IdTag
trait OrderIdTag extends IdTag
trait ProductIdTag extends IdTag

opaque type Id[T <: IdTag] = String

object Id:
  def apply[T <: IdTag](value: String): Id[T] = value
  def generate[T <: IdTag](): Id[T] = UUID.randomUUID().toString

  extension [T <: IdTag](id: Id[T])
    def value: String = id
    def short: String = id.take(8)

// Type aliases for convenience
type UserId = Id[UserIdTag]
type OrderId = Id[OrderIdTag]
type ProductId = Id[ProductIdTag]

object UserId:
  def apply(value: String): UserId = Id[UserIdTag](value)
  def generate(): UserId = Id.generate[UserIdTag]()

object OrderId:
  def apply(value: String): OrderId = Id[OrderIdTag](value)
  def generate(): OrderId = Id.generate[OrderIdTag]()

object ProductId:
  def apply(value: String): ProductId = Id[ProductIdTag](value)
  def generate(): ProductId = Id.generate[ProductIdTag]()

// Domain models using typed IDs
case class User(id: UserId, name: String, email: String)
case class Product(id: ProductId, name: String, price: Double)
case class OrderItem(productId: ProductId, quantity: Int)
case class Order(id: OrderId, userId: UserId, items: List[OrderItem])

// Repository with type-safe lookups
class UserStore:
  private var users = Map.empty[UserId, User]

  def save(user: User): Unit = users += (user.id -> user)
  def findById(id: UserId): Option[User] = users.get(id)
  // findById(orderId)  // Won't compile! Type mismatch

class OrderStore:
  private var orders = Map.empty[OrderId, Order]

  def save(order: Order): Unit = orders += (order.id -> order)
  def findById(id: OrderId): Option[Order] = orders.get(id)
  def findByUser(userId: UserId): List[Order] =
    orders.values.filter(_.userId == userId).toList

@main def run(): Unit =
  val userId = UserId.generate()
  val orderId = OrderId.generate()
  val productId = ProductId.generate()

  println(s"User ID: ${userId.short}...")
  println(s"Order ID: ${orderId.short}...")
  println(s"Product ID: ${productId.short}...")

  // Create entities
  val user = User(userId, "Alice", "alice@test.com")
  val product = Product(productId, "Widget", 29.99)
  val order = Order(orderId, userId, List(OrderItem(productId, 2)))

  // Type-safe stores
  val userStore = UserStore()
  userStore.save(user)
  println(s"Found: ${userStore.findById(userId)}")

  // This would NOT compile:
  // userStore.findById(orderId)  // Type mismatch: OrderId vs UserId
  // userStore.findById(productId)  // Type mismatch: ProductId vs UserId

  val orderStore = OrderStore()
  orderStore.save(order)
  println(s"User orders: ${orderStore.findByUser(userId).size}")

Use Cases

  • Preventing entity ID mixups
  • Domain-driven design
  • Type-safe database lookups

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.