1.2 Hello, Uni!
With Uni on the classpath, the shortest useful program you can write is one that logs a line. That seems small, but it introduces two of Uni's core ideas — the LogSupport trait and source-location capture — and sets up the next step, where we let Design wire objects together for us.
The smallest Uni program
Replace src/main/scala/Main.scala from the previous chapter with:
import wvlet.uni.log.LogSupport
object Main extends LogSupport:
def main(args: Array[String]): Unit =
info("Hello, Uni!")Run it:
$ sbt run
2026-04-18 09:00:00.000-0700 info [Main] Hello, Uni! - (Main.scala:5)Four things are worth pausing on.
LogSupport is a trait, not a wrapper
LogSupport mixes in two things: a logger bound to the enclosing class name, and methods (info, warn, error, debug, trace) that call it. You do not construct a Logger; you declare that this class has one. That is the whole of the logging API for day-to-day code.
The logger name is the class name
The output starts with [Main]. That is the simple name of the class that mixed in LogSupport. When you search logs later, the grep is on a string you can see right here in the source file.
The source location is captured automatically
The (Main.scala:5) at the end of the line is free. Uni captures it at compile time using Scala 3's scala.quoted.Quotes API, so there is no stack-walking cost at runtime. When a production incident makes you ask "which line produced this?", the answer is already printed.
Scala 3 syntax is the default
No new, no class Main { ... } braces, no extends App. The Scala 3 indentation style is what Uni uses in its own code and what this book will use throughout. If you prefer braces, the compiler still accepts them — but the examples here will stay in the indented style.
Adding a collaborator: wire it by hand
A real program has more than one class. Let's add a Greeter and have Main use it.
import wvlet.uni.log.LogSupport
class Greeter extends LogSupport:
def greet(name: String): Unit =
info(s"Hello, ${name}!")
object Main extends LogSupport:
def main(args: Array[String]): Unit =
val greeter = Greeter()
greeter.greet("Uni")Output:
info [Greeter] Hello, Uni! - (Main.scala:5)Two things changed. The logger name shifted to [Greeter] (because the info call now happens inside the Greeter class), and Main now constructs the Greeter explicitly with Greeter().
In a three-line program, manual construction is fine. In a real application it is not:
- Every caller needs to know how every callee is built.
- When a collaborator grows a dependency ("now
Greeterneeds aConfig"), every caller changes. - Swapping an implementation in a test means passing a different constructor argument in every call site.
Uni offers a better tool.
Letting Design do the wiring
The same program, this time wired by Design:
import wvlet.uni.design.Design
import wvlet.uni.log.LogSupport
class Greeter extends LogSupport:
def greet(name: String): Unit =
info(s"Hello, ${name}!")
@main def main(): Unit =
val design = Design.newDesign
.bindSingleton[Greeter]
design.build[Greeter] { greeter =>
greeter.greet("Uni")
}Run it again — same output, but now something structural has changed.
Design.newDesigngives you an empty, immutable wiring configuration..bindSingleton[Greeter]says "when someone asks for aGreeter, build one and share it." No instance is created yet.design.build[Greeter] { greeter => ... }opens a session, builds theGreeter(this is the moment it is actually constructed), hands it to your block, and then shuts the session down when the block returns. Any resources attached to the session are released at that shutdown.
You did not have to tell Main how Greeter is constructed. You did not have to thread a Greeter through intermediate functions. You asked for one, and you got one.
Why this instead of Greeter()?
Fair question — this is exactly the point. With three lines of code, Greeter() is less ceremony. What Design buys you shows up when:
Greetergains a dependency (aConfig, anHttpClient). You only update theDesign, not every call site.- You want a different
Greeterin tests (say, one that captures messages). You replace one line in a test-specific design. - You want to tie shutdown behavior to construction (close the HTTP pool when the session ends). Design has a hook for that.
We will meet all three of those in Part III. For now, treat Design.newDesign.bindSingleton[X].build[X] { x => ... } as the shape your main methods will have from here on.
What you have, what comes next
You can now:
- Print log lines from a Scala 3 program with source locations.
- Construct one object, then three, using
Designinstead of by hand. - Read a Uni-idiomatic
mainmethod and recognize its shape.
The next chapter takes these primitives and builds something you would actually run: a small command-line tool that fetches a URL and prints what it gets back. That single program will introduce argument parsing, HTTP, and the first failure modes Uni helps you handle.
