[Go to site: main page, start]

Java got complicated.We're uncomplicating it.

The language is great. The tooling and ecosystem that grew around it turned into a second job — build files you fight, releases that take a weekend, libraries that drag in the world, and ceremony for the simplest things. Latte brings the simple back.

Dependency management is a second job

Maven wants walls of XML. Gradle makes you pick between two DSLs and then guess which plugin and which configuration block does what. Either way you spend more time feeding the build tool than writing code. Latte uses one readable file and two commands.

Maven — pom.xml
<project>
  <!-- groupId, artifactId, version, properties... -->
  <dependencies>
    <dependency>
      <groupId>org.lattejava</groupId>
      <artifactId>web</artifactId>
      <version>0.1.0</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.13.0</version>
        <configuration><release>25</release></configuration>
      </plugin>
    </plugins>
  </build>
</project>

...and a separate settings.xml, a wrapper script, and a plugin for anything beyond compiling.

Latte — project.latte
dependency(id: "org.lattejava:web:0.1.0")

latte install to fetch it, latte build to build. That's the whole story.

Publishing an artifact shouldn't take a weekend

Shipping a library to Maven Central means a Sonatype account, a published GPG key, signed jars, generated sources and javadoc jars, and the staging "close & release" ritual. Most people give up the first time. With Latte you log in, create a Group, and release.

Maven Central
pom.xml
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-source-plugin</artifactId>
      <version>3.3.1</version>
      <executions><execution><id>attach-sources</id>
        <goals><goal>jar-no-fork</goal></goals></execution></executions>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-javadoc-plugin</artifactId>
      <version>3.11.2</version>
      <executions><execution><id>attach-javadocs</id>
        <goals><goal>jar</goal></goals></execution></executions>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-gpg-plugin</artifactId>
      <version>3.2.7</version>
      <executions><execution><id>sign-artifacts</id>
        <phase>verify</phase><goals><goal>sign</goal></goals></execution></executions>
    </plugin>
    <plugin>
      <groupId>org.sonatype.central</groupId>
      <artifactId>central-publishing-maven-plugin</artifactId>
      <version>0.7.0</version>
      <extensions>true</extensions>
    </plugin>
  </plugins>
</build>
shell
# One-time: publish a GPG key, add a Central Portal token to settings.xml
gpg --gen-key
gpg --keyserver keys.openpgp.org --send-keys <KEY_ID>

# Every release:
git tag v1.0.0 && git push --tags
mvn clean deploy
# then sign in to central.sonatype.com and click "Publish"

A Sonatype account, a GPG key published to a keyserver, credentials in settings.xml, and the staging close & release dance — before mvn deploy publishes anything.

Latte
project.latte
release = loadPlugin(id: "org.lattejava.plugin:release-git:0.3.0")

project(group: "org.example", name: "my-lib", version: "1.0.0", licenses: ["Apache-2.0"]) {
  publishWorkflow {
    latte()
  }
}

target(name: "release", description: "Releases a full version of the project", dependsOn: ["test"]) {
  release.release()
}
shell
latte login     # once

latte release

The release target tags the version from project.latte and publishes to the Latte repository — no keys, no portal, no manual publish step.

Libraries shouldn't drag in the world

Add one library and inherit a dozen more — Jackson, Bouncy Castle, Commons, Guava — each a version conflict and a chunk of supply-chain surface you now own. Latte's libraries ship with zero runtime dependencies. Pure Java, just add a VM.

A typical JWT library — mvn dependency:tree
com.auth0:java-jwt:4.4.0
\- com.fasterxml.jackson.core:jackson-databind:2.15.2
   +- com.fasterxml.jackson.core:jackson-annotations:2.15.2
   \- com.fasterxml.jackson.core:jackson-core:2.15.2

Every transitive jar is a version you must reconcile and a dependency you must trust.

Latte JWT — mvn dependency:tree
org.lattejava:jwt:0.1.1
(no dependencies)

No Jackson, Bouncy Castle, Apache Commons, or Guava. The http server is zero-dependency too.

Frameworks shouldn't be heavyweight

Wiring up one endpoint shouldn't mean a package layout, a wall of imports, component annotations, a reflection-driven container injecting your objects, and a build plugin and launch command just to boot a server. Latte web is a real server in a handful of lines — plain methods, plain calls, no reflection, and one method call to start listening.

Spring Boot
DemoApplication.java
package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
public class DemoApplication {
  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }
}

@Service
class GreetingService {
  String greet(String name) {
    return "Hello, " + name + "!";
  }
}

@RestController
class GreetingController {
  private final GreetingService greetings;

  @Autowired
  GreetingController(GreetingService greetings) {
    this.greetings = greetings;
  }

  @GetMapping("/greeting/{name}")
  public String greeting(@PathVariable String name) {
    return greetings.greet(name);
  }
}
src/main/resources/application.properties
spring.application.name=demo
server.port=8080
shell
# also needs pom.xml (spring-boot-starter-parent, spring-boot-starter-web,
# spring-boot-maven-plugin) and the Maven wrapper (mvnw)
./mvnw spring-boot:run
# downloads the dependency graph, scans for beans, boots embedded Tomcat

Nothing listens until the build file, the plugin, application.properties, and the wrapper are all in place — then a launch command boots the embedded Tomcat.

Latte web
Main.java
import module org.lattejava.http;
import module org.lattejava.web;

String greet(String name) {
  return "Hello, " + name + "!";
}

void main() {
  new Web()
      .get("/greeting/{name}", (req, res) ->
          res.getWriter().write(greet((String) req.getAttribute("name"))))
      .start(8080);   // ← this line starts the HTTP server. That's it.
}
shell
latte run

No servlet container — it runs straight on the Latte http server, which is competitive with Netty and outperforms Jetty and Tomcat in our benchmarks.

Ceremony for the simplest things

For years, printing a line of text meant a public class, a static main method, and a String array you never used. Java 25 fixed the language; Latte leans all the way in — and javaenv means you're never hand-juggling JDK installs to get there.

Classic Java
set up, by hand
# download a JDK from Adoptium, unpack it, and wire up your shell:
mkdir -p ~/dev/java
curl -L "https://api.adoptium.net/v3/binary/latest/25/ga/mac/aarch64/jdk/hotspot/normal/eclipse" \
  | tar -xz -C ~/dev/java
export JAVA_HOME=~/dev/java/jdk-25+36/Contents/Home
export PATH="$JAVA_HOME/bin:$PATH"
mkdir -p src/com/example   # package directories, by hand
src/com/example/Main.java
package com.example;

public class Main {
  public static void main(String[] args) {
    System.out.println("Hello, world!");
  }
}
shell
javac src/com/example/Main.java
java -cp src com.example.Main

The class must match the file name, the package must match the directory, and you manage the JDK, the PATH, and the classpath yourself.

Java 25 + Latte
set up, once
curl -fsSL https://lattejava.org/javaenv/install | bash
curl -fsSL https://lattejava.org/cli/install | bash
javaenv install 25 && javaenv global 25
latte init
Main.java
void main() {
  IO.println("Hello, world!");
}
shell
latte run

javaenv pins the JDK per project and latte init scaffolds the rest — no PATH, no classpath, no boilerplate class. Just write code and latte run.

Get started in five minutes

Install Java, install Latte, create a project, and ship it. No XML. No staging dance.