Skip to Content
Course content

248: Practice Exercise: Modeling an Inventory System with Refined Types

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

I've spent a lot of time maintaining legacy inventory systems, and if there is one thing I've learned, it's that Int and String are far too broad for the real world. In a warehouse, a "quantity" of -5 doesn't make sense, and a "SKU" that's just an empty string is a recipe for a database nightmare. Usually, we handle this with a mountain of if statements or validation logic scattered across the service layer.

Let's try to model a simple product record and see where the cracks appear.

The danger of the naked Int

case class Product(sku: String, quantity: Int, price: Double)

val item = Product(sku = "INVALID_SKU!", quantity = -10, price = -5.0)
println(s"Added $item to inventory")

The compiler is perfectly happy with this. But as far as the business is concerned, this object is a lie. I can pass this item into a shipping function, and suddenly my system thinks I've shipped negative ten items and paid the customer five dollars to take them. I could write a validate() method, but that's an opt-in check. I'd rather make it impossible to even construct an invalid product.

Adding guards with Refined

Since we've touched on the refined library in previous lessons, let's see if we can tighten this up. I want the quantity to be non-negative and the price to be strictly positive. I'll try swapping those primitives for refined types.

import eu.refined.api.Refined
import eu.refined.collection.CSeq
import eu.refined.numeric.Positive
import eu.refined.numeric.NonNegative

case class Product(
  sku: String, 
  quantity: Refined[NonNegative, Int], 
  price: Refined[Positive, Double]
)

// This won't compile if I try to pass a raw Int
// val item = Product("SKU123", 10, 19.99) 

Right away, I hit a wall. I can't just pass 10. The compiler tells me it expects a Refined[NonNegative, Int], not an Int. This is the core trade-off: we've traded convenience for a guarantee. To actually create this object, I have to explicitly refine the value.

Dealing with the runtime reality

Now I have to figure out how to get my raw data (maybe from a JSON API or a database) into these types. I'll use the refineV method, which returns an Either. This is where the "live" part of the validation actually happens.

import eu.refined.refine

def createProduct(sku: String, q: Int, p: Double) = {
  for {
    qty <- refineV[NonNegative](q)
    prc <- refineV[Positive](p)
  } yield Product(sku, qty, prc)
}

val result = createProduct("SKU123", -5, 10.0)
// result is Left(RefinementError(...)) because -5 is not NonNegative


This is much better. The Product case class now acts as a "trusted" object. If I have an instance of Product in my hand, I know for a fact that the quantity isn't negative. I don't have to check it again in the shipItem or calculateValue functions.

Taming the SKU format

The sku is still a raw String, which is bothering me. In my hypothetical warehouse, a SKU must be exactly 5 uppercase letters followed by a hyphen and 4 digits (e.g., "PROD1-1234"). A standard String won't cut it. I need a custom predicate.

I'll try defining a custom refinement. This requires creating a class that extends Predicate.

import eu.refined.Predicate

case class IsSKU() extends Predicate[String] {
  override def refine(value: String): Either[String, String] = {
    if (value.matches("^[A-Z]{5}-\\d{4}$")) Right(value)
    else Left(s"$value is not a valid SKU format (Expected: ABCDE-1234)")
  }
}

// Now let's update our Product model
case class Product(
  sku: Refined[IsSKU, String], 
  quantity: Refined[NonNegative, Int], 
  price: Refined[Positive, Double]
)

Now, my domain model is essentially a set of rules. If I try to create a Product with a SKU like "apple-123", the refineV[IsSKU]("apple-123") call will fail. The beauty here is that the "rules" are now part of the type signature. If I see Refined[IsSKU, String] in a function signature, I don't need to ask the original author "Wait, what's the format for the SKU?"β€”the code tells me exactly what it is.




πŸ“‹ Practical Task

Exercise: Implementing a Validated Warehouse Stock Update

You are building a stock management module. You need to implement a function that updates the quantity of a product, but it must maintain the integrity of the refined types.

Requirements:

  • Use the refined library.
  • Define a Product case class with:
    • sku: Refined as a String that must match the regex "^[A-Z]{3}-\\d{3}$" (e.g., "ABC-123").
    • quantity: Refined as NonNegative Int.
  • Implement a function updateStock(product: Product, change: Int): Either[String, Product].
  • The updateStock function should:
    1. Calculate the new quantity (current quantity + change).
    2. Attempt to refine the new quantity as NonNegative.
    3. If refinement succeeds, return the updated Product.
    4. If refinement fails (i.e., the change would make the stock negative), return a Left with a descriptive error message.

Test Case:
Creating a product with SKU "XYZ-789" and quantity 10, then calling updateStock with -15 should result in a Left error.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.