17 Jul 2025

llm4s.g8: a developer experience boost for LLM4S SDK (issue #94) for Scala devs

🎯 Background

llm4s: Github repo

In the rapidly evolving landscape of software development, where artificial intelligence and language models are reshaping how we build applications, the LLM4S SDK stands out as a powerful toolkit for Scala developers eager to harness the potential of large language models. However, as with many sophisticated tools, the initial onboarding experience can often feel like navigating a labyrinth without a map. This was precisely the challenge highlighted in Issue #94 on the llm4s GitHub repo :

“Using LLM4S code generation requires understanding internal prompts and usage patterns that may not be intuitive for new users. The goal is to improve usability and ergonomics so that Scala developers can easily leverage the power of LLM4S.”

This candid observation resonated deeply with the community and the core development team. While the SDK itself was robust and feature-rich, the friction in getting started was a significant barrier to adoption. It was clear that to unlock the true potential of LLM4S, we needed to transform the onboarding process from a daunting hurdle into a smooth, enjoyable journey.

This blog post chronicles that transformation. It’s a story of turning complexity into clarity, of converting friction into flow, and of crafting not just code, but a developer experience that empowers and inspires. At the heart of this story lies Giter8, a Scala-based templating tool that enables rapid project scaffolding. By leveraging Giter8, we set out to create a starter template-llm4s.g8-that would serve as a beacon for new users, guiding them effortlessly into the world of LLM4S.

Why does developer experience matter?

First impressions are permanent in open source
If a developer can’t get your SDK running in 5 minutes, they leave and never come back. There are no second chances. Your documentation, your setup process, your first-run experience - these are your only shot at adoption. Make them count.

Before diving into the nuts and bolts, it’s worth reflecting on why developer experience (DX) is more than just a buzzword. A stellar DX can be the difference between a tool that’s loved and widely adopted, and one that languishes in obscurity. It’s about reducing cognitive load, eliminating guesswork, and enabling developers to focus on what truly matters: building innovative solutions.

For LLM4S, which sits at the intersection of AI and Scala development, this means providing clear, idiomatic examples, seamless integration with existing workflows, and a project structure that β€œjust works” out of the box. The llm4s.g8 template was conceived as a direct response to these needs-a carefully crafted starter kit that embodies best practices, modern tooling, and a touch of developer empathy.


πŸ›  Building the llm4s.g8 template

Embarking on the creation of the llm4s.g8 template was no mere checkbox exercise. It was a journey marked by iterative refinement, thoughtful design decisions, and a relentless focus on real-world developer workflows. The process unfolded like a narrative of discovery, challenge, and eventual triumph.

The initial spark: Identifying core pain points

The first step was to understand the pain points that new users faced. Through community discussions, issue threads, and direct feedback, several recurring themes emerged:

  • Opaque internal prompts: The SDK’s prompt engineering capabilities were powerful but required deep understanding of the underlying prompt structures.
  • Unintuitive usage patterns: Without a clear example or starter project, users found it difficult to piece together how to invoke and integrate LLM4S features.
  • Fragmented documentation: While comprehensive, the documentation lacked a cohesive β€œgetting started” flow that tied concepts together in a practical manner.
  • IDE friction: The default project layout and dependencies sometimes clashed with common IDE expectations, leading to confusing errors or setup struggles.

Armed with these insights, the team set out to build a solution that would address these issues head-on.

Designing the template: More than just code generation

Templates should ship production-ready examples
Don’t generate TODO comments. Ship working code that calls real APIs, handles real errors, and demonstrates real patterns. Users learn by reading and modifying working code, not by filling in blanks. Make your template the best example in your documentation.

The llm4s.g8 template was envisioned as a multi-faceted toolkit:

  • A clean, runnable Scala project: From the first line of code to the last test, everything should work seamlessly.
  • Correct versions and compatibility: Scala 3, sbt, and JDK versions were carefully selected to maximize compatibility and future-proofing.
  • Practical examples: Including a working Main.scala and PromptExecutor.scala that demonstrate prompt execution using OpenAI’s API, complete with tracing and environment-based API key loading.
  • Testing and CI: Integration tests and GitHub Actions workflows to ensure reliability and quality.
  • Documentation: A comprehensive README that walks users through setup, structure, and publishing.
  • Cross-platform support: Ensuring the template works consistently on Windows, macOS, and Linux.
  • Formatting and style: Integration with scalafmt and MUnit to encourage best practices and maintain code quality.

This was not just scaffolding; it was a developer experience manifesto.

Overcoming technical challenges

Creating a template that β€œjust works” involved navigating several technical hurdles:

  • Template escaping issues: Giter8’s templating syntax can be tricky, especially when embedding code snippets that themselves contain template-like syntax. The team devised clever escaping strategies to ensure smooth generation.
  • Java version alignment: Ensuring the Java version used in the template matched the CI environment was crucial to avoid unexpected runtime issues.
  • CI reliability: Setting up GitHub Actions workflows that validate project generation, compilation, formatting, and testing required meticulous scripting and validation.
  • Dependency management: Adding missing dependencies such as munit for testing, and ensuring all libraries aligned with the Scala 3 ecosystem.

Each challenge was met with a combination of technical rigor and collaborative problem-solving, embodying the spirit of open source development.

The result: A template that inspires confidence

After several iterations, testing cycles, and community feedback rounds, the llm4s.g8 template emerged as a polished, reliable, and user-friendly starting point. It encapsulates the essence of LLM4S while lowering the barrier to entry for newcomers.


βœ… Core deliverables

Giter8 Template (llm4s.g8)

  • Generates a clean, runnable Scala project that users can immediately build and run.
  • Uses correct Scala 3, sbt, and JDK versions to ensure compatibility and performance.
  • Includes a working Main.scala and PromptExecutor.scala that demonstrate prompt execution via OpenAI, complete with tracing and environment-based API key loading (OPENAI_API_KEY).
  • Provides integration tests and CI via GitHub Actions, validating code generation, compilation, formatting, and testing.
  • Contains a detailed README with usage instructions, project structure overview, and publishing guidelines.
  • Supports cross-platform builds and enforces formatting consistency using scalafmt and MUnit test scaffolding.

Main application & prompt executor

The separation of concerns was a deliberate design choice to enhance clarity and maintainability. The Main.scala acts as the entry point, while PromptExecutor.scala encapsulates the logic for interacting with the OpenAI API and executing prompts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Main.scala
object Main {
  def main(args: Array[String]): Unit = {
    val prompt = args.headOption.getOrElse("Explain what a Monad is in Scala")
    PromptExecutor.run(prompt)
  }
}

// PromptExecutor.scala
object PromptExecutor {
  def run(prompt: String): Unit = {
    // Placeholder for prompt execution logic using OpenAI API and tracing
    ...
  }
}

This modular approach not only clarifies responsibilities but also facilitates testing and future extensions.


πŸ“ Directory structure

Every great journey needs a map. Here’s how the llm4s.g8 template scaffolds your project, providing a clear, idiomatic foundation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
.
β”œβ”€β”€ build.sbt
β”œβ”€β”€ project
β”‚   β”œβ”€β”€ build.properties
β”‚   β”œβ”€β”€ plugins.sbt
β”‚   └── default.properties
β”œβ”€β”€ src
β”‚   β”œβ”€β”€ main
β”‚   β”‚   └── scala
β”‚   β”‚       β”œβ”€β”€ Main.scala
β”‚   β”‚       └── PromptExecutor.scala
β”‚   └── test
β”‚       └── scala
β”‚           └── MainSpec.scala
β”œβ”€β”€ .scalafmt.conf
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .github
β”‚   └── workflows
β”‚       └── ci.yml
β”œβ”€β”€ README.md
└── logback.xml

This structure is more than just folders-it’s a launchpad for productivity, discoverability, and collaboration. Each piece is thoughtfully placed to minimize setup friction and maximize clarity.


πŸ›‘ Pre-commit hook setup

Why wait until CI to catch formatting or linting issues? With pre-commit hooks, you get instant feedback before code even leaves your machine. This not only saves time but also keeps your main branch sparkling clean.

The template recommends using pre-commit to automate checks like scalafmt, whitespace, and more. Here’s a sample .pre-commit-config.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
repos:
  - repo: https://github.com/scalameta/scalafmt
    rev: v3.7.14
    hooks:
      - id: scalafmt
        name: scalafmt
        entry: sbt scalafmtAll
        language: system
        types: [scala]
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    hooks:
      - id: end-of-file-fixer
      - id: trailing-whitespace

Benefits:

  • πŸ–– Prevents silly mistakes before they reach CI.
  • 🚦 Encourages a culture of code quality and consistency.
  • 🦾 Automates the boring stuff so you can focus on building.

To enable, simply run:

1
pre-commit install

and enjoy seamless, automated code hygiene.


🧱 build.sbt overview

At the heart of every Scala project lies the build.sbt-your build’s spellbook. In the template, it’s crafted for clarity, flexibility, and Giter8 variable substitution:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
val llm4sVersion = "$llm4s_version$"

ThisBuild / scalaVersion := "$scala_version$"
ThisBuild / organization := "$organization$"

lazy val root = (project in file("."))
  .settings(
    name := "$name$",
    libraryDependencies ++= Seq(
      "com.llm4s" %% "llm4s" % llm4sVersion,
      "org.typelevel" %% "cats-effect" % "$cats_effect_version$",
      "org.scalameta" %% "munit" % "$munit_version$" % Test,
      "com.typesafe.scala-logging" %% "scala-logging" % "$scala_logging_version$",
      "ch.qos.logback" % "logback-classic" % "$logback_version$"
    )
  )

Highlights:

  • Uses Giter8 variables for easy future upgrades and customization.
  • Clearly separates test and main dependencies.
  • Ready for extension-just add modules or plugins as your project grows.

βš™οΈ default.properties variables

The project/default.properties file is the template’s secret sauce for variable management. It defines default values that Giter8 uses for substitution throughout the template:

1
2
3
4
5
6
7
8
llm4s_version=0.7.0
scala_version=3.3.3
cats_effect_version=3.5.1
munit_version=1.0.0-M10
scala_logging_version=3.9.5
logback_version=1.4.14
organization=com.example
name=llm4s-quickstart

Usage:
When you run sbt new llm4s/llm4s.g8, Giter8 replaces variables in build.sbt, README.md, and elsewhere with these values.
For example:

1
val llm4sVersion = "$llm4s_version$" // becomes "0.1.1"

This makes upgrades and customizations a breeze-just bump a version in one place!


πŸ§ͺ Test framework integration

Testing is not an afterthought-it’s baked in from the start. The template integrates MUnit for lightning-fast, expressive Scala testing:

build.sbt:

1
2
libraryDependencies += "org.scalameta" %% "munit" % "$munit_version$" % Test
testFrameworks += new TestFramework("munit.Framework")

Example test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import munit.FunSuite

class MainSpec extends FunSuite {
  test("addition works") {
    assertEquals(2 + 2, 4)
  }
  
  test("Test Prompt executor") {
    val prompt = "Explain what a Monad is in Scala"
    val response = PromptExecutor.run(prompt)
    assert(response.nonEmpty, "Response should not be empty")
    assert(response.contains("Incorrect API key provided: your-api*****here. You can find your API key at https://platform.openai.com/account/api-keys."))
  }
}

Why MUnit?

  • πŸš€ Fast, zero-config, Scala 3-ready.
  • πŸ§ͺ Clean syntax and powerful assertions.
  • 🀝 Easy integration with sbt and CI.

πŸͺ΅ Logging support

Pre-configured logging saves debugging time
Nothing frustrates users more than cryptic errors with no logging context. Ship templates with logging already configured and integrated. When things go wrong (and they will), good logs are the difference between quick fixes and multi-hour debugging sessions.

Every serious application needs robust logging. The template wires in logback-classic for powerful, production-grade logging, and scala-logging for a delightfully idiomatic Scala API.

build.sbt:

1
2
3
4
libraryDependencies ++= Seq(
  "com.typesafe.scala-logging" %% "scala-logging" % "$scala_logging_version$",
  "ch.qos.logback" % "logback-classic" % "$logback_version$"
)

Usage:

1
2
3
4
5
6
7
import com.typesafe.scalalogging.LazyLogging

object Main extends LazyLogging {
  def main(args: Array[String]): Unit = {
    logger.info("Application started!")
  }
}

Extensibility:
Want even richer, type-safe logging? Swap in LogStage for next-level structured logging-your observability future is bright!


πŸ€– GitHub Actions: CI/CD setup

No modern template is complete without CI/CD. The llm4s.g8 template ships with a ready-to-go GitHub Actions workflow that validates every project build, test, and format check:

.github/workflows/ci.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
name: llm4s project giter8 template test

on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]
  workflow_dispatch:  # Allows manual triggering

jobs:
  validate-template:
    name: Test working of the llm4s.g8 template
    strategy:
      fail-fast: false
      matrix:
        os: [ ubuntu-latest ]
        scala: [ 2.13.10 ]
        java: [ 21 ]

    runs-on: ${{ matrix.os }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Set up Java
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: ${{ matrix.java }}
          cache: 'sbt'

      - name: Setup sbt launcher
        uses: sbt/setup-sbt@v1

      - name: Generate project from template
        run: |
          sbt new file://$GITHUB_WORKSPACE/llm4s.g8 \
          --name=llm4s-template \
          --package=org.llm4s.template \
          --version=0.1.0-SNAPSHOT \
          --llm4s_version=0.1.1 \
          --scala_version=2.13.16 \
          --munit_version=1.1.1 \
          --directory=org.llm4s.template \
          --force <<< "\n"

      - name: Check code formatting with scalafmt
        run: |
          cd llm4s-template
          sbt scalafmtCheckAll

      - name: Run compile, test on generated template
        run: |
          cd llm4s-template
          sbt compile test

Result:
Every push or PR is checked for compilation, formatting, and test success, giving you confidence and peace of mind-automatically.


πŸ”Œ plugins.sbt: Plugin magic

SBT plugins unlock powerful capabilities. The template’s project/plugins.sbt brings in the essentials:

1
2
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.11")
  • sbt-scalafmt: Enforces code style and formatting.
  • sbt-ci-release: Simplifies publishing to Maven Central and beyond.

Want more? Just add your favorite plugins-SBT’s ecosystem is your playground.


🎨 Formatting: scalafmt.conf

Code consistency is not just aesthetic-it’s a foundation for collaboration. The template includes a .scalafmt.conf with sensible defaults to keep your codebase tidy:

# scalafmt configuration
version = "3.9.8"
runner.dialect = scala213
maxColumn = 100
style = defaultWithAlign
rewrite.rules = [RedundantParens, RedundantBraces, SortImports, PreferCurlyFors]
spaces.inImportCurlyBraces = true
project.git = true
rewrite.trailingCommas.style = multiple
indentOperator.exemptScope = all

# Optional enhancements
align.preset = more

# Scala‑3 specific (optional)
rewrite.scala3.convertToNewSyntax = true
rewrite.scala3.removeOptionalBraces = true

# Token rewrites
rewriteTokens {
    "β‡’" = "=>",
    "←" = "<-",
    "β†’" = "->";
}

Why it matters:
With scalafmt, every contributor speaks the same code dialect. No more nitpicks-just clean, readable Scala everywhere.


πŸ” Reflections & value

Building the llm4s.g8 template was a journey that transcended mere coding. It was about empathy, community, and the art of crafting experiences that resonate with developers.

The Human factor: Empathy in developer experience

The team recognized that behind every line of code is a developer-sometimes a novice, sometimes a seasoned expert-seeking to understand, experiment, and create. By focusing on empathy, the template was designed to speak the developer’s language, anticipate their needs, and smooth out rough edges.

Value delivered

  • πŸš€ Faster adoption: The one-command starter project slashed onboarding time from around 20 minutes to under 5, a transformational improvement.
  • 🧠 Cleaner learning curve: By providing idiomatic examples and clear structure, the template demystifies prompt engineering and SDK usage.
  • πŸ›  IDE integration-ready: The project layout and dependencies are optimized for popular IDEs, reducing setup friction.
  • βœ… CI-tested reliability: Automated validation builds trust and confidence in the generated projects.
  • 🌍 Cross-platform consistency: Developers on any OS can expect the same seamless experience.
  • πŸ“š Documentation clarity: The README serves as a friendly guide, empowering users to explore and extend with ease.

Beyond the code: Community and collaboration

The creation of llm4s.g8 sparked vibrant discussions in the community, inspiring contributions, feedback, and shared learning. It became more than a template-it became a catalyst for collaboration and innovation.


πŸ“¦ Pull request summary of Issue #94

The pull request associated with Issue #94 represents a significant milestone in the LLM4S project’s evolution. It encapsulates the collective effort to simplify onboarding and improve usability.

βœ… Done

  • Created llm4s.g8: A Giter8-based starter template aligned with LLM4S architecture, featuring Scala 3, sbt, and idiomatic directory layout.
  • Main.scala + PromptExecutor: A working CLI example demonstrating OpenAI prompt execution with tracing support.
  • CLI Support: Commands like sbt run, sbt test, and scalafmtAll work out of the box, with environment-based API key loading (OPENAI_API_KEY).
  • CI Integration: GitHub Actions workflows validate project generation, compilation, formatting, and testing.
  • Integration Testing: Includes scripted and Scala-based tests with cleanup mechanisms to ensure reliability.
  • README.md: Comprehensive usage instructions, directory structure overview, and publishing guide.
  • Added IDE/editor-specific integration helpers, such as VSCode launch configurations.
  • Developing optional template variants, including plugin-based or multi-module setups to cater to diverse use cases.
  • Logger: Added logger / logging support
  • pre‑commit: pre-commit hook setup for automated code checks and formatting.
  • Publishing the template to a Giter8 registry for easier discovery and usage.

βœ… Why it matters

  • Onboarding time reduced significantly, accelerating developer productivity.
  • IDE-friendly scaffolding reduces cognitive load and setup errors.
  • Templates encourage reuse, extension, and experimentation.
  • Cross-platform and CI builds foster early trust and confidence.

❓ Design & Scope Questions

The journey continues with thoughtful questions to guide future directions:

  • Location: Should llm4s.g8 reside under sdk/ or be moved to a root-level /templates/ directory for clearer structure and discoverability?
  • Vision Alignment: Does this approach fully capture LLM4S’s ergonomic goals, or are there opportunities to pivot or expand?
  • Template Scope: Should the project evolve into a family of templates (minimal, plugin-enabled, microservice-ready), or remain a canonical starting point?
  • Target Use Cases: Is the focus on CLI prototyping, code generation experimentation, or learning Scala with LLMs-and how can the template best serve these?

πŸ§ͺ PR Review & Feedback

The review process, enriched by Rory’s insights and Claude Code-assisted analysis, highlighted the strengths and areas for refinement.

βœ… Strengths

  • Addresses real onboarding friction with a practical solution.
  • Solid Giter8 implementation with thoughtful template design.
  • Comprehensive CI and testing coverage ensure reliability.
  • Real-world prompt execution demo showcases SDK capabilities.
  • GitHub Actions workflows validate project generation and build steps.

⚠️ Suggestions

  • build.sbt: Adopt the val llm4sVersion = "$llm4s_version$" syntax for Giter8 variable substitution to improve template flexibility.
  • Test dependencies: Add munit to the build configuration to support testing.
  • Java version alignment: Ensure Java version consistency across CI and template output to prevent runtime issues.

The feedback was embraced as an opportunity to polish and perfect the template further, reinforcing the commitment to quality.


πŸ§ͺ What’s next?

The journey of llm4s.g8 is far from over. Future milestones include:

  • Publishing the template to the official Giter8 registry, making it discoverable and easy to use.
  • Introducing editor launch configurations for VSCode, IntelliJ, and other popular IDEs to streamline debugging and development.
  • Exploring multiple presets and variants to cater to diverse developer needs-from minimal prototypes to plugin-enabled or microservice-ready templates.

πŸ”— CI Build Logs

πŸ‘‰ Want to try it out or contribute?

Dive in by cloning llm4s.g8, running:

1
sbt new llm4s/llm4s.g8

and start hacking away. Your feedback, contributions, and creativity are warmly welcomed as we continue to evolve this exciting project.


In Closing:

The creation of llm4s.g8 is a testament to the power of community-driven innovation and the importance of putting developer experience front and center. By transforming onboarding pain points into a delightful, streamlined process, we have opened the door for more Scala developers to explore, experiment, and build with LLM4S. This is just the beginning of a new chapter-one where the synergy of human creativity and machine intelligence can truly flourish.

Vitthal Mirji profile photo

Vitthal Mirji

Staff Data Engineer @ Walmart

Mumbai, India

Staff Data Engineer & Architect from Mumbai, India. Sharing insights on Data Engineering, Functional programming, Scala, Open source, and life.

Expertise
  • Data Engineering
  • Scala
  • Apache Spark
  • Functional Programming
  • Cloud Architecture
  • GCP
  • Big Data
Next time, we'll talk about "Higher-Order Functions: Regular Functions, But With Trust Issues"