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.
<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.
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.
<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># 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.
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()
}latte login # once
latte releaseThe 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.
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.2Every transitive jar is a version you must reconcile and a dependency you must trust.
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.
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);
}
}spring.application.name=demo
server.port=8080# 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 TomcatNothing listens until the build file, the plugin, application.properties, and the wrapper are all in place — then a launch command boots the embedded Tomcat.
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.
}latte runNo 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.
# 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 handpackage com.example;
public class Main {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}javac src/com/example/Main.java
java -cp src com.example.MainThe class must match the file name, the package must match the directory, and you manage the JDK, the PATH, and the classpath yourself.
curl -fsSL https://lattejava.org/javaenv/install | bash
curl -fsSL https://lattejava.org/cli/install | bash
javaenv install 25 && javaenv global 25
latte initvoid main() {
IO.println("Hello, world!");
}latte runjavaenv 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.