Spider Silk

Spider Silk

Thin call stack, strong signature.

Spider Silk is a web framework built on the Jakarta Servlet API.

Values

Fast startup

An application’s startup time is kept to a minimum. Startup is a cost paid again at every main point of development and operation: autoscaled containers, serverless environments, test runs, and development-server restarts.

Code traceable from its source

The code a request passes through on its way to a response can be followed by reading the source alone. Human developers and AI coding agents both need that traceability. For a person, code that is easy to trace is code that is easy to understand and debug. An AI agent reasons about behavior from the code before running it, so it fills behavior the code does not show with a guess. An agent that guesses wrong about what an annotation does at runtime, for example, writes code that compiles and does not work.

Harmony with the open-source ecosystem

Spider Silk uses proven open-source technology instead of reinventing it. Where several technologies play the same role, it leaves an extension point open for the user to choose one.

Design and technology choices

This section describes the design and the technology the framework chose to realize the three values above.

A simple, intuitive API

Spider Silk keeps its core API intuitive and simple, for traceability. A handler, the core interface, receives the HTTP request as a method parameter of type WebRequest and returns the response as a WebResponse.

The Handler interface
@FunctionalInterface
public interface Handler {
    WebResponse handle(WebRequest request) throws Exception;
}

A handler is mapped to a URL and an HTTP method as a lambda expression or a method reference.

Registering routes
var deckRepository = new DeckRepository(dataSource);
var deckService = new DeckService(deckRepository);
var app = new App();

app.get("/decks/{deckId}", req -> {
    long deckId = req.pathParamLong("deckId");
    Deck deck = deckService.getDeck(deckId);
    return WebResponse.template("deck", Model.of("deck", deck));
});

app.get("/api/decks", req -> {
    JsonArray resBody = Json.array();
    for (DeckSummary deck : deckService.deckSummaries()) {
        resBody.add(Json.object()
            .put("id", deck.id())
            .put("name", deck.name()));
    }
    return WebResponse.json(resBody);
});

app.post("/api/decks", req -> {
    String name = req.bodyJson(json -> json.asObject().getString("name"));
    Deck deck = deckService.createDeck(name);
    JsonObject resBody = Json.object()
        .put("id", deck.id())
        .put("name", deck.name());
    return WebResponse.json(resBody).status(HttpStatus.CREATED);
});

app.start(8080);

No reflection

The Spider Silk framework’s own code uses no reflection. There is no annotation scanning and no proxy generation at application startup, so startup is faster. The framework’s call stack is easy to follow, so the code is more traceable as well. An application also compiles to a GraalVM native image more easily.

A minimal call stack

On its main call path, the handler, the framework adds only two stack frames above HttpServlet.service. The other places the framework calls application code, such as filters, exception handlers, and streamed bodies, stay within four frames. The fewer frames a stack trace holds, the easier the code at fault is to reach, so the code is more traceable. This is a stack trace taken inside a handler.

java.lang.Throwable: where a handler stands
    at com.example.decks.DeckRoutes.one(DeckRoutes.java:15) // the handler
    at net.benelog.spidersilk.AppServlet.dispatch(AppServlet.java:299) // Spider Silk
    at net.benelog.spidersilk.AppServlet.service(AppServlet.java:149) // Spider Silk
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java) // the Servlet API
    at org.eclipse.jetty.ee10.servlet.ServletHolder.handle(ServletHolder.java) // Jetty, from here down

A default stack with a minimal footprint

Spider Silk chose its default stack for fast startup and traceable code. spider-silk-core includes Jetty as its web server and jte as its template engine by default. With one dependency, an application starts with a server and a template engine in place.

Jetty

The default web server is embedded Jetty. Of the three servers, Jetty provides server lifecycle management as its own features most completely. With Jetty, therefore, less of this framework’s code stands between the application and the server than with Tomcat or Undertow. Jetty provides these features on its own:

  • Jetty’s own stopTimeout handles a graceful stop, setStopAtShutdown the shutdown hook, and Jetty’s non-daemon threads keep the JVM alive.

  • It runs without a working directory on disk.

  • Sessions can be turned off with sessions(false).

jte

The default template engine is jte. jte compiles templates to Java source. An expression such as ${deck.name()} therefore runs as a compiled method call. jte does not read model values by reflection, so it fits the no-reflection principle. A precompiled template needs no compiler at runtime, so it works well in a GraalVM native image too. In development, a DirectoryCodeResolver reading the source directory shows an edited template without a rebuild (Development and production).

An extensible module structure

Spider Silk provides optional modules so that widely used technologies beyond the default stack can be used as well. Every module depends on spider-silk-core, and core depends on none of them but silk-json, the JSON library it is built on.

Category Module Contents Dependencies

Default

spider-silk-core

The framework itself. The default web server is Jetty, and the default template engine is jte.

gg.jte:jte, embedded Jetty (jetty-ee10-servlet), silk-json

JSON

silk-json

The JSON library core is built on: writers, readers, the tree, and @JsonBound. It is a library of its own, so an application on another framework can use it too.

the JDK only at run time

spider-silk-jackson

Binds with Jackson through the JsonWriter and JsonReader that handlers take. The ObjectMapper is handed over once, to the constructor of JacksonCodecs.

core, jackson-databind 3.1.x

Web server

spider-silk-tomcat

Runs the same application on embedded Tomcat. It suits an organization that already operates Tomcat, or an application moving over from Spring Boot.

core, tomcat-embed-core 11.0.x

spider-silk-undertow

Runs the same application on embedded Undertow. Undertow’s graceful stop counts requests, so draining works on a virtual-thread executor too.

core, undertow-servlet 2.3.x

Template engine

spider-silk-freemarker

Renders with FreeMarker.

core, freemarker

spider-silk-handlebars

Renders with Handlebars.java.

core, handlebars

spider-silk-thymeleaf

Renders with Thymeleaf.

core, thymeleaf

WebSocket

spider-silk-jetty-websocket

Maps WebSocket endpoints beside the application’s HTTP routes. It is Jetty-only by construction, so it is a module named after Jetty rather than a method of App.

core, jetty-websocket-jetty-server

Testing

spider-silk-test

The WebTest harness and TestRequest, for test scope. It is a separate module, so the production jar carries no test code.

core, and otherwise the JDK only (the servlet API compile-time only, as in core)

Example

example-flashcard

A flashcard study application

core, spring-jdbc, H2

Routing, filters, status pages, JSON, templates, static files, SSE, multipart uploads, and sessions behave the same on all three servers. The APIs of TomcatServer and UndertowServer follow JettyServer’s, so switching servers changes one line: the factory passed to `App.server(…​).

The template engine is switched with App.templates(…​). Switching engines leaves a handler’s WebResponse.template(name, model) call as it is.

The dependencies between the modules are shown in the figure below.

Every module depends on spider-silk-core
Figure 1. The modules and what they depend on

Development tool integration

Spider Silk provides tools for testing, API documentation, observability, builds, and AI coding agents as separate modules and a skill, so that it fits the development tool ecosystem.

Testing
  • The WebTest harness of spider-silk-test starts the application on a free port and hands over a client that keeps cookies.

API documentation
  • spider-silk-openapi turns the route list app.routes() answers into an OpenAPI 3.1 document. Routes are plain data, so the module works without reflection.

Observability
  • spider-silk-opentelemetry-agent is an extension of the OpenTelemetry Java agent that names each server span after the route that answered it, such as GET /api/decks/{deckId}. Without it the agent sees one servlet mapped at /* and names every span GET /*.

Build tools
  • The Gradle plugin net.benelog.spidersilk carries the conventions for precompiled jte templates, Jib container images, and GraalVM native compilation.

  • spider-silk-maven-parent provides the same packaging conventions as a Maven parent POM.

AI coding agents
  • The agent skill lives in skills/spider-silk/ in the Agent Skills format, and guides an agent to use the framework as intended rather than by guessing. Agent Skills is an open standard and the skill uses no feature particular to one tool, so Claude Code, Codex, Cursor, GitHub Copilot, and other AI agents read the same directory as it is. The repository doubles as a plugin marketplace, so Claude Code installs it with two commands:

/plugin marketplace add benelog/spider-silk
/plugin install spider-silk@spider-silk

The repository is a Codex plugin marketplace as well, so Codex installs it with two commands too:

codex plugin marketplace add benelog/spider-silk
codex plugin add spider-silk@spider-silk

Performance

In the benchmark in the appendix, Spider Silk on Jetty answers plain text about 1.42 times as fast as Spring Boot and a JSON list of 100 records about 1.7 times, and its JVM process starts in a third of the time. The benchmark ran both frameworks on one laptop with the same JVM and the same load, against Spring Boot 4.1 with Spring Web MVC, Jackson, and Thymeleaf on its defaults.

Measure Spider Silk (Jetty) Spring Boot

Plain text, requests/s

256,545

180,984

Small JSON object, requests/s

256,318

180,642

JSON list of 100 records, requests/s

163,817

95,200

JSON list of 100 records as a request body, requests/s

114,480

55,860

HTML page, requests/s (jte vs. Thymeleaf)

197,418

63,927

Startup on the JVM

295 ms

902 ms

Startup as a GraalVM native image

24 ms

37 ms

At the JSON list, where the serializer does most of the work, Spider Silk answers about 1.7 times as many requests as Spring Boot with Jackson, and about twice as many when it reads the list from a request body. With Thymeleaf on both sides and the same container, the two render a page within 2% of each other, so the margin on the HTML page is jte’s. As a GraalVM native image, Spider Silk keeps more of its JVM throughput than Spring Boot does, both as a GraalVM CE image and as an Oracle GraalVM image built with profile-guided optimization (PGO). Each cell below is a native image’s throughput as a share of the same application’s on the JVM.

Case Spider Silk, GraalVM CE Spring Boot, GraalVM CE Spider Silk, PGO Spring Boot, PGO

Plain text

75%

65%

93%

84%

Small JSON object

80%

69%

94%

86%

JSON list of 100 records

60%

14%

92%

27%

JSON list of 100 records as a request body

73%

14%

90%

27%

HTML page (jte vs. Thymeleaf)

71%

57%

92%

89%

With Thymeleaf on both sides, Spider Silk’s images keep less: 40% on GraalVM CE and 79% with PGO, against Spring Boot’s 57% and 89%. The appendix covers the method, the machine, the native images, and the limits of these numbers.