The tinystruct framework is designed with a "Simple thinking, Better design" philosophy. It aims to provide a lightweight, high-performance environment for Java development where CLI and Web applications are treated as equal citizens.
- Zero Boilerplate: No
main()method required in your applications. - Unified Design: The same application logic can be invoked via CLI or HTTP.
- Minimal Configuration: Convention over configuration, with an emphasis on transparency.
- High Performance: Optimized for low latency and high throughput.
- JDK 17 or higher.
- Maven for dependency management.
Add the following dependency to your pom.xml:
<dependency>
<groupId>org.tinystruct</groupId>
<artifactId>tinystruct</artifactId>
<version>1.7.28</version>
</dependency>Alternatively, use the tinystruct-archetype to bootstrap a new project.
In tinystruct, every module is an Application. To create one, extend org.tinystruct.AbstractApplication.
package com.example.app;
import org.tinystruct.AbstractApplication;
import org.tinystruct.ApplicationException;
import org.tinystruct.system.annotation.Action;
import org.tinystruct.system.annotation.Action.Mode;
public class HelloApplication extends AbstractApplication {
@Override
public void init() {
// Initialization logic (e.g., setting up resources)
// Note: Do NOT register actions here — use the @Action annotation instead.
this.setTemplateRequired(false); // Skip .view template lookup if returning data directly
}
@Override
public String version() {
return "1.0.0";
}
// Handles: bin/dispatcher hello AND GET /?q=hello
@Action("hello")
public String sayHello() {
return "Hello, tinystruct!";
}
// Path parameter: GET /?q=greet/James OR bin/dispatcher greet/James
@Action("greet")
public String greet(String name) {
return "Hello, " + name + "!";
}
// HTTP-only POST handler
@Action(value = "submit", mode = Mode.HTTP_POST)
public String submit() throws ApplicationException {
// Logic for handling submission
return "Submitted successfully";
}
}The @Action annotation maps URI paths or CLI commands to Java methods.
-
Path Mapping:
@Action("praise")handlesdispatcher praiseor/?q=praise. -
Modes: Specify if an action is restricted to CLI or specific HTTP methods.
-
Metadata: Add descriptions and examples for CLI help generation.
@Action( value = "user/{id}", description = "Get a user by their ID", mode = Mode.HTTP_GET, example = "bin/dispatcher user/42" ) public String getUser(int id) { return "User ID: " + id; }
-
Arguments and Path Parameters: Methods can accept parameters directly. tinystruct automatically builds a regex from the method signature for path parameters (e.g.,
@Action("user/{id}")->^/?user/(-?\d+)$). For complex HTTP interactions, you can includeRequestandResponseas parameters.@Action(value = "upload", mode = Mode.HTTP_POST) public String upload(Request<?, ?> request, Response<?, ?> response) throws ApplicationException { // Use request and response directly for custom logic return "Upload handled"; }
The Context object provides access to request-specific data, including CLI options and HTTP attributes.
@Action("echo")
public String echo() {
// Access CLI flags passed as `--words "Hello World"`
Object words = getContext().getAttribute("--words");
return words != null ? words.toString() : "No words provided";
}The bin/dispatcher tool is the entry point for CLI execution.
- Check Version:
bin/dispatcher --version - Execute Action:
bin/dispatcher hello - Pass Arguments:
bin/dispatcher greet/Jamesorbin/dispatcher echo --words "Praise the Lord"
tinystruct includes a built-in lightweight HTTP server. To start it:
bin/dispatcher start --import org.tinystruct.system.HttpServerAccess your actions via:
http://localhost:8080/?q=hello
Upon successful startup, the server automatically launches the system's default web browser navigated to the server URL (e.g., http://localhost:8080).
- Configuration: This behavior is enabled by default. It can be globally controlled in
application.properties:default.server.open_browser=true - CLI Override: You can override the setting at runtime using the
--open-browserargument:# Disable automatic browser opening on startup bin/dispatcher start --import org.tinystruct.system.HttpServer --open-browser false
tinystruct provides a pluggable architecture for HTTP session management via the SessionManager and SessionRepository interfaces.
- Default Behavior: By default, sessions are stored in memory using
MemorySessionRepository. - Redis Integration: For clustered or stateless deployments, you can easily switch to a Redis-backed session repository.
To configure Redis sessions, update your application.properties:
default.session.repository=org.tinystruct.http.RedisSessionRepository
redis.host=127.0.0.1
redis.port=6379
# redis.password=yourpasswordSession configuration applies universally across all supported server modules (JDK HttpServer, Netty, Tomcat, Undertow).
Handling file uploads is supported out of the box. When a multipart/form-data request is received, the framework automatically parses it. You can access uploaded files using request.getAttachments():
@Action(value = "upload", mode = Mode.HTTP_POST)
public String upload(Request<?, ?> request, Response<?, ?> response) throws ApplicationException {
List<FileEntity> files = request.getAttachments();
if (files != null) {
for (FileEntity file : files) {
System.out.println("Uploaded: " + file.getFilename());
// Save the file to disk
byte[] fileData = file.get();
try (java.io.FileOutputStream fos = new java.io.FileOutputStream(new java.io.File("/tmp/" + file.getFilename()))) {
fos.write(fileData);
} catch (java.io.IOException e) {
throw new ApplicationException(e.getMessage(), e);
}
}
}
return "Upload successful!";
}This generic multipart handler works identically across the built-in JDK HTTP server and specialized server adapters like Undertow and Tomcat.
tinystruct provides built-in support for various databases (H2, MySQL, SQLite, SQLServer).
-
Configure in
application.properties:driver=org.h2.Driver database.url=jdbc:h2:~/test database.user=sa database.password=
-
Usage: Use the
generatecommand to create POJOs and use the internal data layer to interact with the database. -
Exposed Connection Properties: The
DatabaseOperatorclass provides direct, type-safe API methods to query the connection's database metadata, current catalog, and schema name:DatabaseOperator operator = new DatabaseOperator(); try { // Retrieve the active catalog and schema names String catalog = operator.getCatalog(); String schema = operator.getSchema(); // Retrieve full DatabaseMetaData for comprehensive database capabilities and structure info java.sql.DatabaseMetaData metaData = operator.getMetaData(); System.out.println("Database Product Name: " + metaData.getDatabaseProductName()); } finally { operator.close(); }
For JSON serialization and parsing, use the built-in org.tinystruct.data.component.Builder class instead of external libraries like Gson or Jackson.
import org.tinystruct.data.component.Builder;
// Serialization
Builder builder = new Builder();
builder.put("status", "success");
builder.put("data", someObject);
String json = builder.toString();
// Parsing
Builder parsed = new Builder();
parsed.parse(jsonString);
String status = parsed.get("status").toString();Configuration is managed in src/main/resources/application.properties. Key properties include:
driver: Database driver.database.url: JDBC URL.default.home.page: The default action to trigger on the root URL.
tinystruct supports dynamic content through a variable-based templating system.
- Set a Variable:
setVariable("name", "World");
- Template Usage: Variables are replaced in
.viewor HTML files using the[%name%]or similar syntax (depending on the specific template parser).
For clustered environments where you need to synchronize tasks across multiple nodes, use DistributedLock or DistributedRedisLock.
import org.tinystruct.valve.DistributedRedisLock;
import org.tinystruct.valve.Lock;
public void processCriticalTask() {
Lock lock = new DistributedRedisLock("my-global-lock-id");
lock.lock();
try {
// Critical section logic here
} finally {
lock.unlock();
}
}tinystruct provides built-in support for SSE to push real-time updates from server to client.
- Register the client connection:
@Action(value = "stream", mode = Mode.HTTP_GET)
public void stream(Request<?, ?> request, Response<?, ?> response) {
String sessionId = request.getSession().getId();
SSEPushManager.getInstance().register(sessionId, response);
}- Push messages to the client:
Builder message = new Builder();
message.put("event", "update");
message.put("data", "Process completed!");
SSEPushManager.getInstance().push(sessionId, message);The dispatcher provides several utility commands:
generate: POJO object generator for database tables.sql-execute: Run SQL statements directly.install: Install external packages.
Extending ApplicationException or ApplicationRuntimeException allows for structured error reporting across both CLI and Web modes.
tinystruct supports an event-driven architecture to decouple components. To improve performance in asynchronous scenarios, event handlers can offload heavy processing to separate threads.
-
Define an Event: Implement
org.tinystruct.system.Event<T>.public class UserRegisterEvent implements Event<User> { private final User user; public UserRegisterEvent(User user) { this.user = user; } @Override public String getName() { return "user_register"; } @Override public User getPayload() { return user; } }
-
Dispatch an Event:
EventDispatcher.getInstance().dispatch(new UserRegisterEvent(newUser));
-
Asynchronous Handling: To prevent blocking the main thread (e.g., during HTTP requests), handle events asynchronously using
CompletableFuture.EventDispatcher.getInstance().registerHandler(UserRegisterEvent.class, event -> { CompletableFuture.runAsync(() -> { // Heavy tasks: send email, update analytics, etc. sendWelcomeEmail(event.getPayload()); }); });
tinystruct features a robust, zero-configuration-required programmatic logging wrapper around java.util.logging (JUL), managed through application.properties.
- Enable/Disable: Control log generation globally using
logging.enabled:logging.enabled=true - Log Level: Configure the root log level (supports
OFF,SEVERE/ERROR,WARNING/WARN,INFO,CONFIG,FINE/DEBUG,FINER,FINEST/TRACE,ALL):logging.level=INFO - Logger Overrides: Configure specific level settings per package or individual logger class:
org.tinystruct.level=FINE com.example.app.level=WARNING
- ANSI Console Colors: Console output is beautifully formatted and color-coded by log level (red for SEVERE, yellow for WARNING, green for INFO, cyan for CONFIG, and grey for FINE-FINEST debugging logs).
- Precise Caller Tracing: The underlying
LogFormatteruses Java'sStackWalkerAPI to trace the calling stack at runtime. It automatically identifies the actual caller details, injecting the exact class name, method name, file name, and line number into every log record (while bypassing internal helper, framework, and logging classes).
To make outbound HTTP requests, use org.tinystruct.net.URLRequest and org.tinystruct.net.handlers.HTTPHandler.
URL url = new URL("https://api.example.com/data");
URLRequest request = new URLRequest(url);
request.setMethod("POST")
.setHeader("Content-Type", "application/json")
.setBody("{\"key\":\"value\"}");
HTTPHandler handler = new HTTPHandler();
var response = handler.handleRequest(request);
// Always check the status code before using the response body
if (response.getStatusCode() == 200) {
String responseBody = response.getBody();
// Process the successful response
} else {
// Handle the error (e.g., log response.getStatusCode())
}For high-performance or non-blocking I/O, HTTPHandler supports asynchronous requests returning a CompletableFuture.
URL url = new URL("https://api.example.com/data");
URLRequest request = new URLRequest(url);
HTTPHandler handler = new HTTPHandler();
CompletableFuture<URLResponse> future = handler.handleRequestAsync(request);
future.thenAccept(response -> {
if (response.getStatusCode() == 200) {
System.out.println("Async Data: " + response.getBody());
}
}).exceptionally(ex -> {
ex.printStackTrace();
return null;
});tinystruct natively supports the Model Context Protocol (MCP), enabling AI model interactions, tool discovery, and prompt handling.
- Creating an MCP Server:
Extend
MCPServerand register your tools and prompts.
import org.tinystruct.mcp.MCPServer;
import org.tinystruct.mcp.tools.CalculatorTool;
public class MyMCPServer extends MCPServer {
@Override
public void init() {
super.init();
this.registerTool(new CalculatorTool());
}
}- Connecting as a Client:
Use
MCPClientto connect to remote MCP servers, execute tools, and retrieve resources via JSON-RPC.
MCPClient client = new MCPClient("http://localhost:8004", "auth-token");
client.connect();
Map<String, Object> params = new HashMap<>();
params.put("a", 10);
params.put("b", 20);
Object result = client.executeResource("calculator/add", params);
client.disconnect();-
Overloaded Tool Methods Support: tinystruct's MCP implementation natively supports overloaded tool methods (sharing the same tool name but accepting different parameter signatures).
- Schema Merging: When registering tools in
MCPServer, the server automatically merges the input schemas of all overloads into a single unified JSON schema. Properties from all signatures are unioned, and required properties are intersected (so only parameters required by all overloads remain mandatory). - Execution Routing: When the AI client calls a tool, the framework iterates through all available overloaded methods, performing validation against their specific parameter signatures. The first overload that successfully validates against the provided arguments will be executed. If no signature matches or succeeds, the framework throws an appropriate exception (retaining the validation or execution error details).
- Schema Merging: When registering tools in
For testing your applications, use JUnit 5. Since ActionRegistry is a singleton, you must manage its state carefully across tests.
import org.junit.jupiter.api.*;
import org.tinystruct.system.Settings;
class HelloApplicationTest {
private HelloApplication app;
@BeforeEach
void setUp() {
app = new HelloApplication();
// Setting configuration triggers init() and annotation processing
app.setConfiguration(new Settings());
}
@Test
void testHello() throws Exception {
Object result = app.invoke("hello");
Assertions.assertEquals("Hello, tinystruct!", result);
}
@Test
void testGreet() throws Exception {
Object result = app.invoke("greet", new Object[]{"James"});
Assertions.assertEquals("Hello, James!", result);
}
}| Problem | Fix |
|---|---|
ApplicationRuntimeException: template not found |
Call setTemplateRequired(false) in init() if you are returning data directly (e.g., for APIs). |
| Action not found at runtime | Make sure the class is imported via --import on the CLI or listed in application.properties. |
| Method not registered | Ensure the @Action annotation is on a public method — private/protected methods are ignored. |
| CLI arg not visible | Pass arguments with --key value syntax; access via getContext().getAttribute("--key"). |
| JSON using Gson/Jackson | Use org.tinystruct.data.component.Builder instead — it is the framework-native JSON library. |
| Two methods have the same path | Set explicit mode parameters (e.g., Mode.HTTP_GET vs Mode.HTTP_POST) to disambiguate. |
- Granular Applications: Break logic into smaller, focused applications.
- Standard Interfaces: Leverage
init()for setup rather than constructors. - Mode Awareness: Use
Modein@Actionto ensure security (e.g., restricted CLI-only tools).
This project includes a specialized Gemini CLI skill located in .agent/skills/tinystruct-patterns/SKILL.md. This skill provides expert guidance for developing with the tinystruct framework, covering architecture, routing, context, and more.
If you are using Gemini CLI, it will automatically recognize and utilize this skill to assist you with tinystruct-specific development tasks.