| name | lang-scala-library-dev |
| description | Scala-specific library development patterns. Use when creating Scala libraries, designing public APIs with immutability, configuring sbt/Mill build tools, managing cross-Scala version builds, publishing to Maven Central, and writing ScalaDoc. Extends lang-scala-dev with library-specific tooling and patterns. |
Scala Library Development
Scala-specific patterns for library development. This skill extends lang-scala-dev with library tooling, API design patterns, and ecosystem practices for publishing reusable Scala libraries.
This Skill Extends
lang-scala-dev- Foundational Scala patterns (immutability, traits, pattern matching, type system)
For general Scala concepts like case classes, for-comprehensions, and collections, see the base skill first.
This Skill Adds
- Build tooling: sbt and Mill configuration, project structure, multi-module builds
- Library API design: Public API patterns with Scala idioms, binary compatibility
- Publishing: Maven Central publishing, cross-building, versioning strategies
- Documentation: ScalaDoc best practices, documentation generation
- Testing: Library-specific testing patterns, property-based testing
This Skill Does NOT Cover
- General Scala patterns - see
lang-scala-dev - Application development - see
lang-scala-play-devor framework-specific skills - Akka libraries - see
lang-scala-akka-dev - Spark libraries - see
lang-scala-spark-dev
Quick Reference
| Task | sbt Command | Mill Command |
|---|---|---|
| New library project | sbt new scala/scala-seed.g8 |
mill init com-lihaoyi/mill-scala-hello.g8 |
| Compile | sbt compile |
mill _.compile |
| Test | sbt test |
mill _.test |
| Package JAR | sbt package |
mill _.jar |
| Generate docs | sbt doc |
mill _.docJar |
| Publish local | sbt publishLocal |
mill _.publishLocal |
| Publish signed | sbt publishSigned |
mill mill.scalalib.PublishModule/publish |
| Cross build | sbt +compile |
mill __.compile |
| Check binary compat | sbt mimaReportBinaryIssues |
N/A (use sbt-mima plugin) |
Build Tool Configuration
sbt Project Structure
my-library/
├── build.sbt # Build configuration
├── project/
│ ├── build.properties # sbt version
│ ├── plugins.sbt # sbt plugins
│ └── Dependencies.scala # Dependency management (optional)
├── src/
│ ├── main/
│ │ └── scala/ # Library source code
│ ├── test/
│ │ └── scala/ # Tests
│ └── it/ # Integration tests (optional)
│ └── scala/
└── docs/ # Documentation (optional)
build.sbt Configuration
Required fields for publishing:
// build.sbt
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0"
ThisBuild / scalaVersion := "2.13.12"
lazy val root = (project in file("."))
.settings(
name := "my-library",
// Library dependencies
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % "2.10.0",
"org.scalatest" %% "scalatest" % "3.2.17" % Test
),
// Publishing metadata
publishMavenStyle := true,
licenses := Seq("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")),
homepage := Some(url("https://github.com/username/my-library")),
scmInfo := Some(
ScmInfo(
url("https://github.com/username/my-library"),
"scm:git@github.com:username/my-library.git"
)
),
developers := List(
Developer(
id = "username",
name = "Your Name",
email = "you@example.com",
url = url("https://github.com/username")
)
),
// Maven Central publishing
publishTo := {
val nexus = "https://oss.sonatype.org/"
if (isSnapshot.value) Some("snapshots" at nexus + "content/repositories/snapshots")
else Some("releases" at nexus + "service/local/staging/deploy/maven2")
}
)
Cross-Building for Multiple Scala Versions
// build.sbt
lazy val scala213 = "2.13.12"
lazy val scala3 = "3.3.1"
ThisBuild / crossScalaVersions := Seq(scala213, scala3)
ThisBuild / scalaVersion := scala213 // Default
// Version-specific dependencies
libraryDependencies ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, 13)) =>
Seq("org.scala-lang.modules" %% "scala-parallel-collections" % "1.0.4")
case Some((3, _)) =>
Seq.empty // Not needed in Scala 3
case _ =>
Seq.empty
}
}
// Version-specific source directories
Compile / unmanagedSourceDirectories ++= {
val sourceDir = (Compile / sourceDirectory).value
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, n)) => Seq(sourceDir / s"scala-2.$n")
case Some((3, _)) => Seq(sourceDir / "scala-3")
case _ => Seq.empty
}
}
Cross-build commands:
# Compile for all versions
sbt +compile
# Test all versions
sbt +test
# Publish all versions
sbt +publishSigned
Mill Configuration
// build.sc
import mill._, scalalib._, publish._
object mylibrary extends PublishModule with ScalaModule {
def scalaVersion = "2.13.12"
def publishVersion = "0.1.0"
def pomSettings = PomSettings(
description = "My Scala library",
organization = "com.example",
url = "https://github.com/username/my-library",
licenses = Seq(License.`Apache-2.0`),
versionControl = VersionControl.github("username", "my-library"),
developers = Seq(
Developer("username", "Your Name", "https://github.com/username")
)
)
def ivyDeps = Agg(
ivy"org.typelevel::cats-core:2.10.0"
)
object test extends Tests with TestModule.ScalaTest {
def ivyDeps = Agg(
ivy"org.scalatest::scalatest:3.2.17"
)
}
}
Cross-building with Mill:
// build.sc
import mill._, scalalib._
val scala213 = "2.13.12"
val scala3 = "3.3.1"
trait MyModule extends ScalaModule with PublishModule {
def publishVersion = "0.1.0"
// ... common settings
}
object mylibrary extends Cross[MyLibraryModule](scala213, scala3)
class MyLibraryModule(val crossScalaVersion: String) extends MyModule {
def scalaVersion = crossScalaVersion
}
Library API Design
Public API Patterns
Prefer immutable data types:
// Good: Immutable case class
case class Config(
timeout: Duration,
retries: Int,
baseUrl: String
)
// Modification returns new instance
val updated = config.copy(retries = 5)
// Avoid: Mutable class
class Config {
var timeout: Duration = _
var retries: Int = _
// ...
}
Use sealed traits for ADTs:
sealed trait Result[+A]
case class Success[A](value: A) extends Result[A]
case class Failure(error: String) extends Result[Nothing]
case object Pending extends Result[Nothing]
// Exhaustive pattern matching
def handle[A](result: Result[A]): String = result match {
case Success(value) => s"Got: $value"
case Failure(error) => s"Error: $error"
case Pending => "Waiting..."
}
Builder pattern for complex configuration:
case class HttpClient private (
timeout: Duration,
retries: Int,
followRedirects: Boolean,
userAgent: String
)
object HttpClient {
def builder(): Builder = Builder()
case class Builder private[HttpClient] (
timeout: Duration = Duration(30, TimeUnit.SECONDS),
retries: Int = 3,
followRedirects: Boolean = true,
userAgent: String = "MyLibrary/1.0"
) {
def withTimeout(timeout: Duration): Builder = copy(timeout = timeout)
def withRetries(retries: Int): Builder = copy(retries = retries)
def withFollowRedirects(follow: Boolean): Builder = copy(followRedirects = follow)
def withUserAgent(ua: String): Builder = copy(userAgent = ua)
def build(): HttpClient = HttpClient(timeout, retries, followRedirects, userAgent)
}
}
// Usage
val client = HttpClient.builder()
.withTimeout(Duration(60, TimeUnit.SECONDS))
.withRetries(5)
.build()
Type classes for extensibility:
trait Encoder[A] {
def encode(value: A): String
}
object Encoder {
def apply[A](implicit enc: Encoder[A]): Encoder[A] = enc
def instance[A](f: A => String): Encoder[A] = new Encoder[A] {
def encode(value: A): String = f(value)
}
// Instances
implicit val intEncoder: Encoder[Int] = instance(_.toString)
implicit val stringEncoder: Encoder[String] = instance(identity)
// Derived instance
implicit def optionEncoder[A](implicit enc: Encoder[A]): Encoder[Option[A]] = {
instance {
case Some(value) => enc.encode(value)
case None => "null"
}
}
}
// Usage
def toJson[A: Encoder](value: A): String = {
Encoder[A].encode(value)
}
API Stability and Versioning
Use @deprecated for gradual migration:
object MyLibrary {
@deprecated("Use newMethod instead", "1.2.0")
def oldMethod(): Unit = newMethod()
def newMethod(): Unit = {
// New implementation
}
}
Package private for internal APIs:
// Visible only within package
private[mylibrary] class InternalHelper {
// ...
}
// Visible to this module only
private[this] val internalState = mutable.Map.empty[String, Int]
Use opaque types (Scala 3) for type safety:
// Scala 3
opaque type UserId = Long
object UserId {
def apply(value: Long): UserId = value
extension (id: UserId) {
def toLong: Long = id
}
}
// Cannot accidentally pass Long where UserId expected
val userId: UserId = UserId(123L)
val rawId: Long = userId.toLong
Binary Compatibility
MiMa (Migration Manager for Scala)
Setup in build.sbt:
// project/plugins.sbt
addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.3")
// build.sbt
import com.typesafe.tools.mima.plugin.MimaPlugin.autoImport._
lazy val root = (project in file("."))
.enablePlugins(MimaPlugin)
.settings(
mimaPreviousArtifacts := Set(organization.value %% name.value % "0.1.0"),
// Binary compatibility checks
mimaReportBinaryIssues := {
mimaReportBinaryIssues.value
// Fail build on incompatibilities
},
// Allow specific breakages
mimaBinaryIssueFilters ++= Seq(
// Example: Allow removal of private class
ProblemFilters.exclude[MissingClassProblem]("com.example.internal.PrivateClass")
)
)
Check compatibility:
# Report binary compatibility issues
sbt mimaReportBinaryIssues
# Allow breaking changes for major version
sbt "set mimaPreviousArtifacts := Set()" publishLocal
Compatibility Guidelines
| Change | Binary Compatible? | Source Compatible? |
|---|---|---|
| Add method to class | ✓ Yes | ✓ Yes |
| Add method to trait | ✗ No (before Scala 2.12) | ✓ Yes |
| Remove public method | ✗ No | ✗ No |
| Add parameter with default | ✓ Yes | ✓ Yes |
| Add parameter without default | ✗ No | ✗ No |
| Change return type | ✗ No | ✗ No |
| Make final class | ✗ No | Depends |
| Seal trait | ✗ No | ✗ No |
| Add case to sealed trait | ✗ No | ✗ No |
| Widen visibility | ✓ Yes | ✓ Yes |
| Narrow visibility | ✗ No | ✗ No |
Publishing to Maven Central
Setup Requirements
- Create Sonatype JIRA account: https://issues.sonatype.org/
- Request namespace (e.g.,
com.github.usernameorio.github.username) - Setup GPG key for signing artifacts
- Configure credentials
GPG Signing
Generate GPG key:
# Generate key
gpg --gen-key
# List keys
gpg --list-keys
# Export public key to keyserver
gpg --keyserver keyserver.ubuntu.com --send-keys YOUR_KEY_ID
Configure sbt-pgp:
// project/plugins.sbt
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1")
// build.sbt
useGpg := true // Use GPG command-line tool
Credentials Configuration
Create ~/.sbt/1.0/sonatype.sbt:
credentials += Credentials(
"Sonatype Nexus Repository Manager",
"oss.sonatype.org",
"your-sonatype-username",
"your-sonatype-password"
)
Or use environment variables:
credentials += Credentials(
"Sonatype Nexus Repository Manager",
"oss.sonatype.org",
sys.env.getOrElse("SONATYPE_USERNAME", ""),
sys.env.getOrElse("SONATYPE_PASSWORD", "")
)
Publishing Workflow
Using sbt-sonatype plugin:
// project/plugins.sbt
addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.10.0")
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1")
// build.sbt
import xerial.sbt.Sonatype._
sonatypeProjectHosting := Some(GitHubHosting("username", "project", "you@example.com"))
sonatypeCredentialHost := "s01.oss.sonatype.org" // For new projects
publishTo := sonatypePublishToBundle.value
Publish commands:
# 1. Update version in build.sbt (remove -SNAPSHOT for release)
# 2. Create git tag
git tag -a v0.1.0 -m "Release 0.1.0"
# 3. Publish and sign
sbt +publishSigned
# 4. Release to Maven Central (bundle workflow)
sbt sonatypeBundleRelease
# Or manual workflow:
# sbt sonatypeClose # Close staging repo
# sbt sonatypeRelease # Release to Maven Central
# 5. Push tag
git push origin v0.1.0
Release Checklist
- Update version in build.sbt (remove
-SNAPSHOT) - Update CHANGELOG.md
- Run tests:
sbt +test - Check binary compatibility:
sbt mimaReportBinaryIssues - Build for all Scala versions:
sbt +package - Generate and check docs:
sbt doc - Create git tag:
git tag -a vX.Y.Z -m "Release X.Y.Z" - Publish signed artifacts:
sbt +publishSigned - Release to Maven Central:
sbt sonatypeBundleRelease - Push tag:
git push origin vX.Y.Z - Create GitHub release with release notes
- Bump version to next SNAPSHOT:
X.Y.Z-SNAPSHOT
ScalaDoc
ScalaDoc Syntax
/**
* Parses a JSON string into a case class.
*
* This method uses the implicit [[Decoder]] to convert the JSON string
* into the target type `A`.
*
* @param json the JSON string to parse
* @tparam A the target type (must have an implicit Decoder)
* @return a [[scala.util.Try]] containing the parsed value or error
* @throws IllegalArgumentException if the JSON is malformed
* @see [[Decoder]] for information on creating custom decoders
* @example
* {{{
* case class Person(name: String, age: Int)
* implicit val decoder: Decoder[Person] = ...
*
* val result = parseJson[Person]("""{"name":"Alice","age":30}""")
* // result: Success(Person("Alice", 30))
* }}}
*/
def parseJson[A: Decoder](json: String): Try[A] = ???
ScalaDoc Tags
| Tag | Purpose | Example |
|---|---|---|
@param |
Parameter description | @param name the user's name |
@tparam |
Type parameter | @tparam A the element type |
@return |
Return value | @return the parsed result |
@throws |
Exception thrown | @throws IOException if file not found |
@see |
Reference | @see [[OtherClass]] |
@example |
Code example | @example {{{ ... }}} |
@note |
Important note | @note This method is thread-safe |
@since |
Version added | @since 1.2.0 |
@deprecated |
Deprecation notice | @deprecated("Use newMethod", "1.3.0") |
Documentation Generation
sbt:
# Generate API docs
sbt doc
# Open in browser
open target/scala-2.13/api/index.html
# Generate for all Scala versions
sbt +doc
Mill:
# Generate docs
mill mylibrary.docJar
# Extract and view
unzip out/mylibrary/docJar.dest/out.jar -d docs
Package-Level Documentation
Create package.scala:
/**
* Core library for JSON parsing and serialization.
*
* == Overview ==
* This package provides type-safe JSON encoding and decoding using type classes.
*
* == Quick Start ==
* {{{
* import com.example.json._
*
* case class User(name: String, age: Int)
* implicit val decoder = Decoder.derive[User]
*
* val json = """{"name":"Alice","age":30}"""
* val user = parseJson[User](json)
* }}}
*
* @see [[Encoder]] for creating custom encoders
* @see [[Decoder]] for creating custom decoders
*/
package object json {
// Package-level type aliases
type Result[A] = Either[JsonError, A]
}
Testing Patterns
Unit Testing with ScalaTest
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class ConfigSpec extends AnyFlatSpec with Matchers {
"Config" should "parse valid configuration" in {
val config = Config.parse("timeout=30,retries=3")
config shouldBe defined
config.get.timeout shouldBe Duration(30, TimeUnit.SECONDS)
config.get.retries shouldBe 3
}
it should "reject invalid timeout values" in {
val config = Config.parse("timeout=-1")
config shouldBe empty
}
it should "use default values when not specified" in {
val config = Config.parse("")
config shouldBe defined
config.get.retries shouldBe 3 // Default
}
}
Property-Based Testing with ScalaCheck
import org.scalacheck.Properties
import org.scalacheck.Prop.forAll
object JsonPropertiesSpec extends Properties("Json") {
property("roundtrip") = forAll { (user: User) =>
val json = toJson(user)
val parsed = fromJson[User](json)
parsed == Right(user)
}
property("never crashes") = forAll { (s: String) =>
try {
fromJson[User](s)
true
} catch {
case _: Exception => false
}
}
}
Integration Testing
Create src/it/scala/ directory:
// src/it/scala/HttpClientIntegrationSpec.scala
import org.scalatest.flatspec.AnyFlatSpec
class HttpClientIntegrationSpec extends AnyFlatSpec {
"HttpClient" should "make real HTTP requests" in {
val client = HttpClient.builder().build()
val response = client.get("https://httpbin.org/get")
assert(response.status == 200)
}
}
Configure in build.sbt:
lazy val IntegrationTest = config("it") extend Test
lazy val root = (project in file("."))
.configs(IntegrationTest)
.settings(
Defaults.itSettings,
IntegrationTest / scalaSource := baseDirectory.value / "src/it/scala"
)
// Run integration tests
// sbt it:test
Multi-Module Projects
sbt Multi-Module Setup
// build.sbt
lazy val commonSettings = Seq(
organization := "com.example",
scalaVersion := "2.13.12",
version := "0.1.0"
)
lazy val core = (project in file("core"))
.settings(
commonSettings,
name := "my-library-core",
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % "2.10.0"
)
)
lazy val http = (project in file("http"))
.dependsOn(core)
.settings(
commonSettings,
name := "my-library-http",
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-dsl" % "0.23.23"
)
)
lazy val json = (project in file("json"))
.dependsOn(core)
.settings(
commonSettings,
name := "my-library-json",
libraryDependencies ++= Seq(
"io.circe" %% "circe-core" % "0.14.6"
)
)
lazy val root = (project in file("."))
.aggregate(core, http, json)
.settings(
commonSettings,
name := "my-library",
publish / skip := true // Don't publish root
)
Module commands:
# Build specific module
sbt core/compile
# Test all modules
sbt test
# Publish specific module
sbt core/publishSigned
# Publish all modules
sbt +publishSigned
Mill Multi-Module Setup
// build.sc
import mill._, scalalib._
trait CommonModule extends ScalaModule {
def scalaVersion = "2.13.12"
def publishVersion = "0.1.0"
}
object core extends CommonModule {
def ivyDeps = Agg(
ivy"org.typelevel::cats-core:2.10.0"
)
}
object http extends CommonModule {
def moduleDeps = Seq(core)
def ivyDeps = Agg(
ivy"org.http4s::http4s-dsl:0.23.23"
)
}
object json extends CommonModule {
def moduleDeps = Seq(core)
def ivyDeps = Agg(
ivy"io.circe::circe-core:0.14.6"
)
}
Anti-Patterns
1. Breaking Binary Compatibility
// v1.0.0
trait Parser {
def parse(input: String): Result
}
// v1.1.0 - WRONG! Breaks binary compatibility
trait Parser {
def parse(input: String): Result
def parseWithOptions(input: String, options: Options): Result
}
// v1.1.0 - Correct: Provide default implementation
trait Parser {
def parse(input: String): Result
def parseWithOptions(input: String, options: Options): Result = {
// Default implementation
parse(input)
}
}
2. Exposing Mutable Collections
// Bad: Exposes mutable collection
class Registry {
private val items = mutable.ListBuffer.empty[Item]
def getItems: mutable.ListBuffer[Item] = items // Dangerous!
}
// Good: Return immutable view
class Registry {
private val items = mutable.ListBuffer.empty[Item]
def getItems: List[Item] = items.toList // Safe copy
}
3. Overusing Implicits
// Bad: Too many implicit conversions
implicit def intToString(x: Int): String = x.toString
implicit def stringToInt(s: String): Int = s.toInt
// Good: Explicit type classes
trait Show[A] {
def show(a: A): String
}
implicit val intShow: Show[Int] = (a: Int) => a.toString
References
lang-scala-dev- Foundational Scala patterns- sbt Documentation
- Mill Documentation
- Maven Central Publishing Guide
- MiMa GitHub
- ScalaDoc Style Guide
- Scala Library Design Guidelines