Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

I remember a few years ago working on a high-frequency trading engine. We had IDs for everything—Instruments, Orders, Accounts—and for the sake of performance, we represented them all as Longs. One afternoon, a bug slipped through where a developer accidentally swapped the orderId and the instrumentId in a cancellation call. The compiler didn't blink because, as far as it was concerned, a Long is a Long. We spent three hours chasing a "missing order" ghost, only to realize we were essentially telling the system to cancel an instrument instead of a specific order. We tried using case class wrappers, but the JVM's boxing behavior in our hot loops caused a spike in GC pressure that we simply couldn't afford.

Stopping the ID Swap

The problem we faced is called "primitive obsession." It's that tempting habit of using a basic type like String or Int to represent a domain concept. While it's easy to start with, it leaves you vulnerable to the exact kind of logic error I described. You want the type safety of a wrapper—where the compiler screams at you if you put an AccountId where a TransactionId belongs—but you don't want the runtime overhead of creating a new object every time you wrap a value.

This is where opaque type aliases come in. They allow you to define a type that is represented by another type at runtime, but the compiler treats it as a distinct type everywhere else. It's the best of both worlds: the performance of a primitive and the safety of a dedicated class.

Zero-Cost Abstractions with Opaque Types

To use an opaque type, you define it within an object. The key is that the "opaqueness" is scoped to that object. Inside the object, the type is transparent; outside, it's a black box.

object DomainTypes {
  opaque type UserId = Long
  
  def mkUserId(id: Long): UserId = id
}

Now, look at what happens when we try to use this in our main logic. If I import UserId and the mkUserId factory method, I can create users, but I can't treat them like numbers anymore.

import DomainTypes.*

val user1 = mkUserId(12345L)
val user2 = mkUserId(67890L)

// This will fail to compile:
// val sum = user1 + user2 
// Error: value + is not a member of UserId

The JVM sees a long, but the Scala compiler sees a UserId. I love this because it forces me to be explicit about my domain logic without paying a "performance tax" in the form of heap allocations.

Controlling the Leak

Since the type is transparent inside the defining object, that's where you put your "privileged" operations. You decide exactly which transformations are allowed. If you want to allow UserIds to be converted back to Longs for a database driver, you provide a specific method for it. If you don't provide that method, the rest of your application is physically unable to treat the ID as a number.

object DomainTypes {
  opaque type UserId = Long
  
  def mkUserId(id: Long): UserId = id
  
  // We explicitly allow this "leak" for persistence layers
  def toLong(id: UserId): Long = id
}

By restricting the API surface area, you've effectively created a firewall around your primitives. You're no longer relying on your teammates (or your future, tired self) to remember which Long is which; you're letting the compiler enforce the rules of your business domain.




📋 Practical Task

Implementing a Secure Session and Email Type System

You are building a user authentication module. Currently, both SessionToken and UserEmail are being passed around as Strings, which has led to several bugs where tokens were accidentally logged as emails.

Your task is to:

  • Create a AuthTypes object.
  • Define opaque type aliases for SessionToken and UserEmail, both backed by String.
  • Implement factory methods (e.g., mkToken and mkEmail) to create these types.
  • Create a function validateSession(token: SessionToken, email: UserEmail): Boolean.
  • In your main code, demonstrate that attempting to pass a UserEmail into the token argument of validateSession results in a compilation error.
  • Implement a revealToken(token: SessionToken): String method inside the AuthTypes object to show how to safely extract the underlying value when necessary.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.