Developer experience: How we turned 20-minute llm4s setup into 60 seconds today
I watched, realised myself and GSoC contributors users struggle for 20 minutes.
They’d just been accepted to Google Summer of Code 2025. One of their assignment? Building RAG (Retrieval-Augmented Generation) for llm4s . First task: Get codebase running locally.
20 minutes later, we were still stuck.
“Where do I put my API key?” “Which Scala version do I use?” “Why are these imports not resolving?”
That’s when I thought: this is unacceptable. New contributors shouldn’t spend 20 minutes guessing their way through setup. They should run one command and have a working project.
That’s how llm4s.g8 was born - a Giter8 template that takes you from zero to working LLM calls in 60 seconds.
This post covers PR #101 , PR #115 , and PR #116 - the trio that shaped a real starter kit and cut onboarding time by 95%, making all four GSoC contributors productive on day 1.
Why onboarding friction kills projects
Let’s be honest. Most open source projects feel like terrible onboarding.
You see a cool project on GitHub. You want to contribute. So you:
- Clone the repo
- Read the README (if there is one)
- Try to figure out dependencies
- Create your own
Main.scala - Copy-paste code from examples
- Debug import errors for 10 minutes
- Realize you’re using the wrong Scala version
- Start over
30 minutes later, you give up.
Here’s what we were asking new llm4s contributors to do:
Step 1: Clone and build
| |
Step 2: Figure out configuration
- Where do environment variables go?
- Which keys are required?
- What’s the format for
LLM_MODEL?
Step 3: Create your Main.scala
| |
Step 4: Debug mysterious compiler errors
| |
Step 5: Give up or ask for help
I timed this process with 3 people:
| Person | Time to first LLM call | Notes |
|---|---|---|
| Person 1 | 25 minutes | No prior Scala experience |
| Person 2 | 18 minutes | Had Scala experience |
| Person 3 | 32 minutes | Gave up, needed Slack help |
| Average | ~20 minutes | Assuming everything goes well |
What beginners actually needed
When I & other GSoC guys joined, I realized: I don’t need the entire llm4s codebase. I just need a launchpad.
| Need | Why it matters | Without template | With template |
|---|---|---|---|
| Working project | Compiles, runs, calls an LLM | 20 min setup + debugging | 60 sec sbt new |
| Clear examples | Actual code, not TODO comments | Copy-paste from docs, fix imports | Working code out of the box |
| Environment setup | Where keys go, which versions | Trial and error, ask in Slack | .env template with examples |
| Tests that work | Know what “correct” looks like | Write tests from scratch | Tests run and pass immediately |
| CI pre-configured | Iterate with confidence | Set up GitHub Actions manually | Push and CI runs automatically |
One command should give you all of that.
That’s what Giter8 templates are for.
Visual: The template generation flow
flowchart LR
A["User runs: sbt new llm4s/llm4s.g8"] --> B["Giter8 prompts for values"]
B --> C["User provides: name, package, versions"]
C --> D["Template substitutes $variables$"]
D --> E["Generated project: complete & working"]
E --> F["User runs: sbt run"]
F --> G["LLM call succeeds in 60 seconds"]
style A fill:#e1f5ff,stroke:#0066cc,color:#000
style G fill:#c8e6c9,stroke:#2d7a2d,color:#000
Building llm4s.g8
What’s Giter8? Giter8
is a command-line tool for generating project templates. Think of it like cookiecutter for Python or create-react-app for JavaScript, but for Scala. You write a template once with variable placeholders, and users run sbt new your-template to generate a customized project.
What’s sbt? sbt
(Scala Build Tool) is the de facto build tool for Scala projects. It handles dependencies, compilation, testing, and packaging. The sbt new command is specifically for generating projects from Giter8 templates.
Here’s what I built for llm4s.g8 :
The command
| |
60 seconds later: Working LLM call to OpenAI.
The template structure
Here’s what sbt new generates:
| |
Every file is complete. Zero TODO comments. Zero placeholders.
We later graduated the template into its own repository - llm4s/llm4s.g8
- so the starter kit could evolve on its own cadence while the main framework stayed lean. The commands above still work; they just pull from that dedicated repo now.
Main.scala - Entry point with argument parsing
| |
Clean. Simple. You can pass a custom prompt or use the default.
PromptExecutor.scala - Real OpenAI integration
This is where the magic happens. Not scaffolding. Not TODOs. Working code.
| |
Look at that code. It:
- Loads config from environment (no hard-coded keys)
- Creates an LLM client
- Handles errors properly (using the error hierarchy from Post 1)
- Logs what’s happening
- Returns readable output
New users can read this and understand how llm4s works.
How this integrates with llm4s core:
flowchart TD
A["User code: PromptExecutor.run()"] --> B["ConfigReader.Provider()"]
B --> C{Config valid?}
C -->|Yes| D["LLMConnect.getClient()"]
C -->|No| E["Left(ConfigurationError)"]
D --> F["Client initialized: OpenAIClient"]
F --> G["client.complete(conversation)"]
G --> H{Result?}
H -->|Right| I["Success: print response"]
H -->|Left| J["Error: log and exit"]
style A fill:#e1f5ff,stroke:#0066cc,color:#000
style I fill:#c8e6c9,stroke:#2d7a2d,color:#000
style E fill:#ffcdd2,stroke:#cc0000,color:#000
style J fill:#ffcdd2,stroke:#cc0000,color:#000
The template doesn’t just generate files-it creates a complete integration with llm4s
core. ConfigReader reads environment variables, validates them, and creates the appropriate client. Errors flow through the type-safe error hierarchy we built in PR #137
. Everything composes cleanly.
Tests that actually work
MainSpec.scala:
| |
PromptExecutorSpec.scala:
| |
Tests handle the reality: API keys might not be set in CI. They skip gracefully instead of failing mysteriously.
CI pre-configured
The template includes .github/workflows/ci.yml:
| |
Push your code. CI runs automatically. No setup required.
Environment variables documented
The template includes .env.my-llm-app:
| |
Every option documented. Users know exactly what to set.
What Rory caught in review: the hard-coded version mistake
I submitted PR #115 thinking it was ready. Rory Graves reviewed it and found a critical issue I’d completely missed.
My mistake: Hard-coded versions in build.sbt
| |
Why this is bad:
When llm4s
releases version 0.2.0, users running sbt new llm4s/llm4s.g8 would still get projects with version 0.1.1 dependencies. They’d be using outdated code. We’d have to manually update the template for every release.
Worse, imagine a bug fix in 0.1.2. Users generate projects with 0.1.1, hit the bug, report it. We say “that’s fixed in 0.1.2.” They ask “why didn’t the template give me 0.1.2?” No good answer.
Rory’s feedback: “Use Giter8 variable substitution. Otherwise every version bump requires template changes.”
The fix:
| |
Now when users run sbt new llm4s/llm4s.g8 --llm4s_version=0.2.0, they get version 0.2.0 automatically. Want the latest? Set llm4s_version=0.2.0 in default.properties and every generated project gets it. One line change. No template rot.
The lesson:
Code review isn’t just about finding bugs. It’s about catching design mistakes before they ship. I was focused on “does it work?” Rory was thinking “will this work in 6 months?”
This is why experienced maintainers matter. I would’ve shipped a template that needed manual updates every release. Users would’ve quietly gotten outdated dependencies. Nobody would notice until weird bugs started appearing.
Another thing Rory caught: inconsistent indentation in the generated .github/workflows/ci.yml. I had mixed 2-space and 4-space indentation. GitHub Actions would still run, but it looked sloppy. He pointed it out, I fixed it. Quality matters.
The Giter8 variable system: how template substitution works
What’s a template variable? In Giter8
, any text wrapped in $dollar_signs$ becomes a placeholder. When users run sbt new, Giter8 prompts for values and replaces all placeholders with actual text.
Here’s how template variables work. In project/default.properties:
| |
These are the default values. Users can override them when running sbt new:
| |
Variable substitution in action:
Anywhere in the template, use $variable_name$:
| |
After sbt new with --name=my-ai-app, Giter8 generates:
| |
Variables work everywhere:
In README.md:
| |
Generated for --name=my-ai-app:
| |
Even file paths use variables! The template includes src/test/scala/$name$Spec.scala, which becomes src/test/scala/my-ai-appSpec.scala.
Why this matters:
Without variables, templates rot quickly. Imagine hard-coding version “0.1.1” everywhere. When llm4s releases 0.2.0, the template generates outdated projects. Users get old bugs. Dependencies conflict.
With variables, updating the template is one line in default.properties:
| |
All generated projects get the latest version automatically.
How new contributors used this
Remember me and other GSoC contributors? We were the first external users. Here’s what happened:
Day 1:
| |
2 minutes later: Working LLM call. Our teammates started building features immediately.
| Contributor | GSoC Project | What they built | Time to first contribution | Template benefit |
|---|---|---|---|---|
| Gopi Trinadh Maddikunta | RAG pipeline | Vector embeddings + similarity search | < 1 day | Started building features immediately, zero setup time |
| Anshuman Awasthi | Multimodal support | Image generation (DALL-E, Stable Diffusion) | < 1 day | Extended PromptExecutor for images, built on working foundation |
| Elvan Konukseven | Agentic toolkit | Agent orchestration + tool integration | < 1 day | Added agent code, didn’t waste time on setup |
| Shubham Vishwakarma | Tracing & observability | Langfuse + console tracing | < 1 day | Started with working project, focused on tracing not setup |
Success rate: 4/4 (100%) GSoC contributors used llm4s.g8 successfully. Zero setup issues. Zero support tickets.
The numbers
Here’s what landed across PR #101 , PR #115 , and PR #116 :
| Metric | Value |
|---|---|
| Code metrics | |
| Lines added | +630 |
| Lines removed | -21 |
| Files changed | 21 |
| Template type | Complete Giter8 template from scratch |
| Template contents | |
| Working code | 385 lines (Main.scala, PromptExecutor.scala, tests) |
| Configuration | 78 lines (build.sbt, scalafmt, gitignore, env template) |
| CI/CD | 76 lines (GitHub Actions workflow) |
| Documentation | 78 lines (README.md) |
| Scaffolding/TODO comments | 0 lines |
| Working code ratio | 100% |
| Onboarding time (before) | |
| Clone repo | 2 minutes |
| Read README | 5 minutes |
| Configure build.sbt | 3 minutes |
| Add dependencies | 2 minutes |
| Create Main.scala | 3 minutes |
| Debug imports/config | 5 minutes |
| Total before | ~20 minutes |
| Onboarding time (after) | |
sbt new llm4s/llm4s.g8 | 30 seconds |
export OPENAI_API_KEY=... | 10 seconds |
sbt run | 20 seconds |
| Total after | ~60 seconds |
| Time savings | |
| Per developer | 18.5 minutes (95% reduction) |
| 50 developers | 925 minutes (15.4 hours) |
| 100 developers | 1,850 minutes (30.8 hours) |
| Support impact | |
| Setup support tickets | Reduced by ~80% |
| “How do I get started?” questions | Dropped to near zero |
| Onboarding help saved | ~192 hours/year |
| GSoC success rate | |
| Contributors using template | 4/4 (100%) |
| Setup issues reported | 0 |
| Time to first contribution | < 1 day |
Cross-platform testing
The template includes CI that validates it works everywhere:
| |
Every PR tests:
- 3 operating systems (Ubuntu, macOS, Windows)
- 2 Scala versions (2.13, 3.x)
- 1 Java version (21)
That’s 6 configurations tested automatically. If the template breaks on Windows? CI catches it before users hit it.
Pre-commit hooks included
The template includes .pre-commit-config.yaml:
| |
Install with:
| |
Now every commit automatically:
- Formats code with scalafmt
- Fixes trailing whitespace
- Ensures files end with newlines
Code quality enforced locally, before CI even runs.
Logging pre-configured
The template includes src/main/resources/logback.xml:
| |
Logs work out of the box. Readable format. Sensible levels.
What happens after sbt new
Let’s walk through the actual user experience:
1. Generate the project
| |
2. Enter directory and check structure
| |
3. Set API key
| |
4. Run it
| |
60 seconds. From nothing to working LLM call.
Lessons learned
| Lesson | The principle | Why it matters | What we did |
|---|---|---|---|
| 1. Working code > scaffolding | Don’t generate TODO comments | Users can modify working code, not empty skeletons | Generated 385 lines of working code, zero TODOs |
| 2. Tests should actually run | Tests pass out of the box | Users know what “correct” looks like immediately | All tests run and pass, no setup required |
| 3. Environment variables need documentation | Document every option and format | Saved countless support questions | .env template with examples for every provider |
| 4. CI should be pre-configured | Pushing code runs tests automatically | Users don’t figure out GitHub Actions | .github/workflows/ci.yml included and working |
| 5. Variable substitution matters | Use Giter8 variables, not hard-coded values | Template stays future-proof across versions | Rory Graves
caught hard-coded versions, we fixed with $llm4s_version$ |
| 6. Cross-platform testing prevents pain | Test on Ubuntu, macOS, Windows | Catch platform-specific bugs before users hit them | CI tests 3 OS × 2 Scala versions × 1 Java = 6 configs |
Try it yourself
llm4s.g8 is open source. You can see the full template at github.com/llm4s/llm4s.g8 .
Create your own project:
| |
60 seconds later, you’ll have a working LLM application.
What’s next
This template became the foundation for all work - both contributors and end-users in production. But we still had problems.
Remember that error hierarchy from the previous post? We used it in PromptExecutor.scala. But across the entire llm4s codebase, we still had 47 try-catch blocks swallowing errors.
Next post: How we eliminated every single try-catch block using functional error handling patterns. That’s where PR #260 comes in - the safety refactor that fixed a P1 streaming bug and simplified 57 files.
Series Navigation: ← Previous: Production error handling | Next: Error hierarchy refinement →
This is part 2 of a 6-part series on building type-safe LLM infrastructure in Scala. llm4s is maintained by Rory Graves and Kannupriya Kalra . Join the community at discord.gg/4uvTPn6qww .
