Skip to content
lang-scala-library-dev logo

Scala Library Development

lang-scala-library-dev

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 a...

SKILL.md

Full skill instructions

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-dev or framework-specific skills
  • Akka libraries - see lang-scala-akka-dev
  • Spark libraries - see lang-scala-spark-dev

Quick Reference

Tasksbt CommandMill Command
New library projectsbt new scala/​scala-seed.g8mill init com-lihaoyi/​mill-scala-hello.g8
Compilesbt compilemill _.compile
Testsbt testmill _.test
Package JARsbt packagemill _.jar
Generate docssbt docmill _.docJar
Publish localsbt publishLocalmill _.publishLocal
Publish signedsbt publishSignedmill mill.scalalib.PublishModule/​publish
Cross buildsbt +compilemill __.compile
Check binary compatsbt mimaReportBinaryIssuesN/​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:[email protected]:username/​my-library.git"
      )
    ),
    developers := List(
      Developer(
        id    = "username",
        name  = "Your Name",
        email = "[email protected]",
        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

ChangeBinary 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✗ NoDepends
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

  1. Create Sonatype JIRA account: https://issues.sonatype.org/
  2. Request namespace (e.g., com.github.username or io.github.username)
  3. Setup GPG key for signing artifacts
  4. 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", "[email protected]"))
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

TagPurposeExample
@paramParameter description@param name the user's name
@tparamType parameter@tparam A the element type
@returnReturn value@return the parsed result
@throwsException thrown@throws IOException if file not found
@seeReference@see [[OtherClass]]
@exampleCode example@example {{{ ... }}}
@noteImportant note@note This method is thread-safe
@sinceVersion added@since 1.2.0
@deprecatedDeprecation 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

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View