This codebase provides the facilities necessary to build a web application, with a design that prioritizes server-side-rendering. Basic capabilities that any web application would need, like persistent storage or templating, are provided. See Features further down in this document.
You might find this project well-suited to your needs if your priority is maintainability and quality, and if you are aiming to build programs that last years with minimal upkeep.
There are several examples of projects built with this framework in the example projects below.
Here is a small Minum program (see more code samples below):
public class Main {
public static void main(String[] args) {
var minum = FullSystem.initialize();
var wf = minum.getWebFramework();
wf.registerPath(GET, "",
r -> Response.htmlOk("
Hi there world!
"));
minum.block();
}
}
| Capability | Rating |
|---|---|
| Small | [#######-] |
| Tested | [#######-] |
| Documented | [######--] |
| Performant | [######--] |
| Maintainable | [#######-] |
| Understandable | [#######-] |
| Simple | [######--] |
| Capable | [######--] |
make test_coverage)make mutation_test)Minum has zero dependencies, and is built of ordinary and well-tested code: hashmaps, sockets, and so on. The "minimalist" competitors range from 400,000 to 700,000 lines when accounting for their dependencies.
Applying a minimalist approach enables easier debugging, maintainability, and lower overall cost. Most frameworks trade faster start-up for a higher overall cost. If you need sustainable quality, the software must be well-tested and documented from the onset. As an example, this project's ability to attain such high test coverage was greatly enabled by the minimalism paradigm. The plentiful tests and comments help to make intent and implementation clearer.
Minum follows semantic versioning.
1) Make it work. 2) Make it right. 3) Make it fast.
This project represents thousands of hours of an experienced practitioner experimenting with maintainability, performance, and pragmatic simplicity.
There is a 🚀 Quick start, or if you have a bit more time, consider trying the tutorial
<dependency>
<groupId>com.renomad</groupId>
<artifactId>minum</artifactId>
<version>10.0.4</version>
</dependency>
Secure TLS 1.3 HTTP/1.1 web server
A web server is the program that enables sending your data over the internet.
In-memory database with disk persistence
A database is necessary to store data for later use, such as user accounts. Our database stores all its data in memory, but writes it to the disk as well. The only time data is read from disk is at database startup. There are benefits and risks to this approach.
Server-side templating
A template is just a string with areas you can replace, and the template processor renders these quickly. For example, here is a template: "Hello {{ name }}". If we provide the template processor with that template and say that "name" should be replaced with "world", it will do so.
Logging
Logs are text that the program outputs while running. There can be thousands of lines output while the program runs.
Testing utilities
The test utilities are mostly ways to confirm expectations, and throw an error if unmet.
HTML parsing
Parsing means to interpret the syntax and convert it to meaningful data for later analysis. Because web applications often have to deal with HTML, it is a valuable feature in a minimalist framework like this one.
Background queue processor
Across the majority of the codebase, the only time code runs is when a request comes in. The background queue processor, however, can continue running programs in parallel.
Compiled size: 209 kilobytes.
Lines of production code (including required dependencies)
| Minum | Javalin | Spring Boot |
|---|---|---|
| 6,715 | 141,048 | 1,085,405 |
See a size comparison in finer detail
Performance is a feature. On your own applications, collect performance metrics at milestones, so that trends and missteps are made apparent.
One of the benefits of minimalism combined with test-driven development is that finding the bottlenecks and making changes is easier, faster, and safer.
See a Minum versus Spring performance comparison
See the following links for sample projects that use this framework.
This project is valuable to see the minimal-possible application that can be made. This might be a good starting point for use of Minum on a new project.
This is a good example to see a basic project with various functionality. It shows many of the typical use cases of the Minum framework.
This is a large and sophisticated program, about 15,000 lines of code, and is an example of the intended end-goal state of a Minum application. It is a family-tree video and sharing application.
Restaurants is a prototype project to provide a customizable ranked list of restaurants. On this project's README, there is a link to a video of its construction.
This is a project which uses a SQL database called H2, and which shows how a user might go about including a different database than the one built-in.
The following code samples help provide an introduction to the features.
Instantiating a new database:
There are two database options - the older simpler database, or the new DbEngine2 database. They have nearly identical interfaces and external behaviors, but the DbEngine2 database reads and writes to disk magnitudes faster than its sibling Db. It is the recommended database to use going forward.
Here is how you instantiate the faster database:
AbstractDb<Foo> db = new DbEngine2<>(foosDirectory, context, new Foo());
Here is the simpler database:
AbstractDb<Foo> db = new Db<>(foosDirectory, context, new Foo());
The Minum database keeps its data and processing primarily in memory but persists to the disk. Data is only read from disk at startup.
There are pros and cons to this design choice: on the upside, it's very fast and the data stays strongly typed. On the downside, it does not make the same guarantees as ACID-compliant databases. Also, an ill-considered application design could end up using too much memory.
On the Memoria project, the risks and benefits were carefully considered, and so far it has worked well.
Users are free to pick any other database they desire (See "Alternate database" project above for an example project using a third-party database).
Checksum
On the DbEngine2 database engine (the recommended engine for most users),
there is a "checksum" feature. In short, a hash will be generated on data
when written, and checked against the file when read. This confirms the
data has not changed since it was written, which could indicate data
corruption. If that is discovered, an exception will be thrown when the
data is loaded. For that reason, it is recommended to use the following
pattern when instantiating (note the loadData at the end):
AbstractDb<Foo> db = new DbEngine2<>(foosDirectory, context, new Foo()).loadData();
If registering indexes (see the section on indexes later on), this pattern is recommended:
AbstractDb<Foo> db = new DbEngine2<>(foosDirectory, context, new Foo())
.registerIndex("id", x -> x.getId().toString())
.loadData();
Adding a new object to a database:
// instantiate an object that implements DbData. Note that because
// this is a new object, we set the index to 0.
Foo myFoo = new Foo(0L, 42, "blue");
// write it to the database. The returned value is the object we wrote
// with its index set by the database.
Foo myResultingFoo = db.write(myFoo);
Updating an object in a database:
myResultingFoo.setColor("orange");
db.write(myResultingFoo);
Deleting from a database:
db.delete(myResultingFoo);
Getting a bit more advanced - indexing:
If there is a chance there might be a large amount of data and search speed is important, it may make sense to register indexes. This can be best explained with a couple examples.
1) If every item has a unique identifier, like a UUID, then this is how you would register the index for much-increased performance in getting a particular item (compared to a worst-case full table scan)
In the following example, we will have a database of "Foo" items, each of which has a UUID identifier.
// The first parameter is the name of the index, which we will refer to elsewhere when
// asking for our data, and the second parameter is the function used to generate the key
// for the internal map.
db.registerIndex("id", x -> x.getId().toString())
// later to get the data, it will look like this, and will return
// a collection of data. If the identifier is truly unique, then we can
// prefer to use the findExactlyOne method
db.findExactlyOne("id", "87cfcbc1-5dad-4dcd-b4dc-7d8da9552ffc");
2) alternately, instead of having one-to-one unique values, we might be partitioning the data in some way. For example, the data may be categorized by color.
// note that the key generator (the second parameter here) must always return a string
db.registerIndex("colors", x -> x.getColor().toString())
// later to get the data, it will look like this, and will return
// a collection of data. If the identifier is truly unique, then we can
// prefer to use the findExactlyOne method
Collection<Foo> blueFoos = db.getIndexedData("colors", "blue");
Writing a log statement:
// useful info for comprehending what the system is doing, non-spammy
logger.logDebug(() -> "Initializing main server loop");
The logs are output to "standard out" during runtime. This means, if you run a Minum application from the command line, it will output its logs to the console. This is a typical pattern for servers.
The logs are all expecting their inputs as closures - the pattern is
() -> "hello world". This keeps the text from being processed until
it needs to be. An over-abundance of log statements could impact the
performance of the system. By using this design pattern, log
statements will only be run if necessary, which is
$ claude mcp add minum \
-- python -m otcore.mcp_server <graph>