Field Notes

Scala/Play, Part 9: Scala Traits for Java Developers - Contracts and Shared Behavior

Scala/Play, Part 9: Scala Traits for Java Developers - Contracts and Shared Behavior

I started noticing traits and implicits in Scala code at work before we reached them in this course. Traits turned out to be a reassuring place to start: as a Java/Spring developer, I already understood contracts and implementations. The new keyword became much less mysterious once I connected it to a small repository.

This Scala 2.13.18 lesson defines a policy lookup contract, implements it with a List, then gives the trait a working method. The policies are fictional. There is no database or framework wiring in this example.

Zakaria's smiling navy-hoodie mascot fitting blue gears into a brass mechanism inside a sunlit clock tower
A specific component fitting into a larger mechanism. In our code, the implementation supplies the lookup that shared behavior calls.

A trait declares what a repository can do

trait PolicyRepository {
  def find(number: String): Option[PolicySnapshot]
}

There is no equals sign or body after find. It is an abstract method: a concrete implementing class must supply the lookup. The contract accepts a String and promises an Option[PolicySnapshot], so absence is explicit.

PolicySnapshot is the case class from our earlier lessons, with number, basePremium and claimFreeYears fields. The trait does not choose how those snapshots are stored.

The Java comparison is straightforward:

public interface PolicyRepository {
    Optional<PolicySnapshot> find(String number);
}

This is a useful starting comparison, not a claim that Scala traits and Java interfaces have identical capabilities in every situation.

The implementation uses a method we already know

class InMemoryPolicyRepository(policies: List[PolicySnapshot])
    extends PolicyRepository {

  override def find(number: String): Option[PolicySnapshot] =
    policies.find(policy => policy.number == number)
}

Scala uses extends here where Java would use implements. The constructor receives our policies list. Without val or var, this parameter does not expose a public policies property.

The method body is List.find from Part 6: return the first matching snapshot as Some, or None when no match exists. We have introduced a new boundary around familiar behavior.

I wrote override to make the relationship explicit. In Scala 2 it is optional when implementing an abstract method. When replacing an inherited concrete method, it is required.

The declared type is the contract

val policies = List(
  PolicySnapshot("POL-001", 600, 3),
  PolicySnapshot("POL-002", 600, 2)
)

val repository: PolicyRepository =
  new InMemoryPolicyRepository(policies)

repository.find("POL-001")
// Some(PolicySnapshot(POL-001,600,3))

repository.find("POL-999")
// None

The variable has declared type PolicyRepository. The actual object is an InMemoryPolicyRepository. Calling find through the trait executes the implementation on that object. This is the polymorphism I already know from Java.

That lets the calling code ask for the contract:

def describe(
  number: String,
  repository: PolicyRepository
): String =
  repository.find(number)
    .map(policy => "Found policy " + policy.number)
    .getOrElse("Policy not found")

The caller turns an optional policy into display text. The repository performs the lookup. We pass the dependency explicitly as an ordinary argument; nothing is discovered or injected automatically.

The describe caller depends on PolicyRepository while InMemoryPolicyRepository implements its find method using List.find
The caller uses the contract; the runtime object supplies the lookup behavior.

A trait can also provide the method body

Now I add an existence check directly to the trait:

trait PolicyRepository {
  def find(number: String): Option[PolicySnapshot]

  def exists(number: String): Boolean =
    find(number).isDefined
}

find is still abstract. exists is concrete because it has a body. It calls find and checks whether its result is defined: Some gives true, None gives false.

InMemoryPolicyRepository stays unchanged. It inherits exists, so both calls now work:

repository.exists("POL-001") // true
repository.exists("POL-999") // false

The point that matters is where the inner call goes. The inherited exists body executes on the repository object. Its find call therefore reaches InMemoryPolicyRepository.find. The trait can define shared behavior using a method whose implementation it leaves to a class.

Call sequence: repository.exists invokes the trait body, which calls the concrete find implementation, then isDefined converts Some to true or None to false
exists supplies the shared operation; find supplies the storage-specific step.

The Java default-method connection

public interface PolicyRepository {
    Optional<PolicySnapshot> find(String number);

    default boolean exists(String number) {
        return find(number).isPresent();
    }
}

For this example, a Java default interface method is the closest comparison. Scala needs no default keyword: the method body makes exists concrete. A class can inherit it or replace it with an override.

A practical detail follows from our implementation: exists performs the find lookup each time. It is not a cached flag. A future storage implementation could provide a specialized exists method, but a future asynchronous repository would also require reconsidering this synchronous contract. Merely keeping the name would not make the APIs interchangeable.

Run the repository example

TraitLesson.scala at this lesson's commit exercises found and missing policies, plus an empty repository. From the lab root:

sbt "runMain learning.PremiumLesson"
Through the trait: Found policy POL-001
Missing number: Policy not found
Empty repository: Policy not found
Known policy exists: true
Missing policy exists: false
Policy in empty repository exists: false

The run passed our assertions and the earlier lesson checks. I now have a concrete distinction between an abstract declaration, an implementation, and inherited behavior. Next comes multiple parameter lists, followed by implicit parameters using this same repository contract.

Reference: Scala 2 Book: traits with abstract and concrete methods. Our course continues toward Scala 2.13.18, Play 3 and Apache Pekko.