How to Use Protocol Buffers in Scala

A practical guide to working with Protocol Buffers in Scala using ScalaPB

Published: January 2025 • 10 min read

Protocol Buffers works excellently with Scala, especially for big data processing, Akka applications, and microservices. Scala developers use ScalaPB, which generates idiomatic Scala case classes with immutability and type safety built in.

This guide walks you through using Protocol Buffers in Scala. We'll use simple examples and focus on practical implementation. Scala's functional nature makes working with protobuf clean and elegant.

What You'll Need

  • Scala: Version 2.12+ or 3.x (check with scala -version)
  • sbt: Scala build tool version 1.5+
  • ScalaPB: We'll add this as an sbt plugin

Step 1: Set Up Your Project

Create a new Scala project:

sbt new scala/scala-seed.g8
cd scala-protobuf-demo

mkdir -p src/main/protobuf

Add ScalaPB to your project/plugins.sbt:

addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.0.6")

libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.11.13"

Update your build.sbt:

name := "scala-protobuf-demo"
version := "0.1.0"
scalaVersion := "2.13.12"

// Enable ScalaPB
Compile / PB.targets := Seq(
  scalapb.gen() -> (Compile / sourceManaged).value / "scalapb"
)

// Add ScalaPB runtime library
libraryDependencies ++= Seq(
  "com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf"
)

Step 2: Create Your .proto File

Create src/main/protobuf/subscriber.proto:

syntax = "proto3";

package telecom;

// Mobile subscriber information
message Subscriber {
  string msisdn = 1;           // Mobile number
  string name = 2;             // Subscriber name
  string email = 3;            // Email address
  SubscriptionType type = 4;   // Plan type
  bool active = 5;             // Account status
  repeated string services = 6; // Active services
}

enum SubscriptionType {
  PREPAID = 0;
  POSTPAID = 1;
  CORPORATE = 2;
}

Step 3: Generate Scala Code

Compile the proto file to generate Scala classes:

sbt compile

ScalaPB automatically generates Scala case classes in target/scala-*/src_managed/main/scalapb/

Step 4: Use Protocol Buffers in Scala

Create src/main/scala/Main.scala:

import telecom.subscriber._

object Main extends App {
  // Create a subscriber using case class syntax
  val subscriber = Subscriber(
    msisdn = "+91-9876543210",
    name = "Telecom Customer",
    email = "customer@telecom.com",
    type = SubscriptionType.POSTPAID,
    active = true,
    services = Seq("Voice", "Data", "4G LTE")
  )

  println(s"Created subscriber: ${subscriber.name}")
  println(s"MSISDN: ${subscriber.msisdn}")
  println(s"Type: ${subscriber.type}")

  // Serialize to binary
  val bytes = subscriber.toByteArray
  println(s"Serialized to ${bytes.length} bytes\n")

  // Deserialize from binary
  val decoded = Subscriber.parseFrom(bytes)
  println(s"Deserialized subscriber: ${decoded.name}")
  println(s"Email: ${decoded.email}")
  println(s"Active: ${decoded.active}")

  // Access repeated fields (services)
  println("\nActive services:")
  decoded.services.foreach(service => println(s"  - $service"))
}

Step 5: Run Your Application

Run the application:

sbt run

You should see output like:

Created subscriber: Telecom Customer
MSISDN: +91-9876543210
Type: POSTPAID
Serialized to 68 bytes

Deserialized subscriber: Telecom Customer
Email: customer@telecom.com
Active: true

Active services:
  - Voice
  - Data
  - 4G LTE

Immutable Updates with Copy

ScalaPB generates case classes, so you can use Scala's copy method:

val subscriber = Subscriber(
  msisdn = "+91-9876543210",
  name = "Original Name",
  type = SubscriptionType.PREPAID
)

// Update fields immutably
val updated = subscriber.copy(
  name = "Updated Name",
  type = SubscriptionType.POSTPAID,
  active = true
)

println(s"Original: ${subscriber.name}, ${subscriber.type}")
println(s"Updated: ${updated.name}, ${updated.type}")

// Add services
val withServices = updated.copy(
  services = updated.services :+ "5G"
)
println(s"Services: ${withServices.services.mkString(", ")}")

Saving and Loading from Files

Save and load protobuf data:

import java.nio.file.{Files, Paths}
import telecom.subscriber._

object FileExample extends App {
  val subscriber = Subscriber(
    msisdn = "+91-9123456789",
    name = "Network Admin",
    email = "admin@telecom.com",
    type = SubscriptionType.CORPORATE,
    active = true
  )

  // Save to file
  val bytes = subscriber.toByteArray
  Files.write(Paths.get("subscriber.bin"), bytes)
  println("Saved to file")

  // Load from file
  val loadedBytes = Files.readAllBytes(Paths.get("subscriber.bin"))
  val loaded = Subscriber.parseFrom(loadedBytes)
  println(s"Loaded: ${loaded.name}")
  println(s"MSISDN: ${loaded.msisdn}")
}

JSON Conversion

ScalaPB supports JSON conversion. Add the JSON dependency to build.sbt:

libraryDependencies += "com.thesamet.scalapb" %% "scalapb-json4s" % "0.12.0"

Use it in your code:

import scalapb.json4s.JsonFormat
import telecom.subscriber._

val subscriber = Subscriber(
  msisdn = "+91-9876543210",
  name = "Mobile User",
  type = SubscriptionType.PREPAID,
  active = true
)

// Convert to JSON
val jsonString = JsonFormat.toJsonString(subscriber)
println("JSON output:")
println(jsonString)

// Parse from JSON
val jsonData = """
{
  "msisdn": "+91-9111111111",
  "name": "New Subscriber",
  "type": "POSTPAID",
  "active": true
}
"""

val parsed = JsonFormat.fromJsonString[Subscriber](jsonData)
println(s"\nParsed from JSON: ${parsed.name}")
println(s"Type: ${parsed.type}")

Using Protobuf with Akka

Protobuf works great with Akka actors for message passing:

import akka.actor.{Actor, ActorSystem, Props}
import telecom.subscriber._

class SubscriberActor extends Actor {
  def receive = {
    case subscriber: Subscriber =>
      println(s"Received subscriber: ${subscriber.name}")
      println(s"MSISDN: ${subscriber.msisdn}")
      println(s"Type: ${subscriber.type}")
      
      // Process the subscriber
      val updated = subscriber.copy(active = true)
      sender() ! updated
  }
}

object AkkaExample extends App {
  val system = ActorSystem("TelecomSystem")
  val actor = system.actorOf(Props[SubscriberActor], "subscriber")
  
  val subscriber = Subscriber(
    msisdn = "+91-9876543210",
    name = "Akka User",
    type = SubscriptionType.POSTPAID
  )
  
  actor ! subscriber
  
  Thread.sleep(1000)
  system.terminate()
}

Best Practices for Scala

Use Case Classes Naturally

ScalaPB generates immutable case classes. Use pattern matching, copy methods, and functional transformations just like regular Scala code.

Leverage Type Safety

Scala's type system catches many protobuf errors at compile time. Take advantage of sealed traits for enums and Option types for optional fields.

Use Functional Operations

Repeated fields are Scala Seq, so usemap,filter, and other collection operations.

Keep Proto Files Simple

ScalaPB generates more code than Java. Keep your proto files focused and avoid overly complex nested structures.

Common Issues

Issue: Generated classes not found

Solution: Make sure to run sbt compile first. ScalaPB generates code during compilation. In IntelliJ, you may need to reimport the project.

Issue: Version conflicts

Solution: Make sure your ScalaPB plugin version matches the runtime library version. Check the official ScalaPB docs for compatible versions.

Issue: sbt compilation slow

Solution: ScalaPB generates a lot of code. Use sbt ~compile for incremental compilation during development.

Related Tools

Additional Resources

Official Documentation & References

Conclusion

Protocol Buffers and Scala are a natural fit. ScalaPB generates idiomatic, type-safe code that feels like native Scala. Whether you're building Spark jobs, Akka systems, or microservices, protobuf offers excellent performance with Scala's functional programming benefits.

Start with these examples and integrate protobuf into your Scala applications. The combination of protobuf's efficiency and Scala's expressiveness creates powerful, maintainable systems.