Exploring Application Layering in distlang with simpleApp
Microservices became popular because they helped teams split systems into smaller parts that were easier to own, deploy, and scale. That kind of separation is useful, but it also introduces a different kind of complexity. Once behavior is spread across multiple services or components, more effort goes into coordination, communication, retries, shared state, and failure handling. In many systems, the operational burden comes less from the business logic itself and more from the plumbing between parts.
One way to manage that complexity is to build software in layers of abstraction. A lower layer can provide a capability. A higher layer can use that capability to define a more complete application pattern. If that higher layer proves useful, it can become the foundation for still more specialized layers above it.
That is the direction we are exploring in distlang. In the previous post, we introduced ObjectDB as a helper that gives applications a simple storage capability. You can think of it as a lower layer in the system. It provides persistence without forcing application code to absorb provider-specific database wiring.
simpleApp is a higher layer built on top of that kind of capability. Instead of exposing only storage operations, it starts to describe a reusable application shape. It gives us a way to compose multiple handler sets around shared infrastructure and shared state, while keeping each part focused on a narrower responsibility. The diagram below shows how simpleApp works, with each app containing multiple handlers. After building, you get two different deployments of those handlers talking to the same shared database.

The Echo app is a small example of this layering model. It is intentionally simple, but it shows the direction clearly. One part of the app accepts configuration and stores it. Another part reads that configuration and uses it to shape the response. The important point is not the echo behavior itself. The important point is that a higher-level application layer can coordinate multiple handlers by relying on a lower-level storage layer. The diagram below shows how the configuration handlers and the app-specific logic can be expressed in one file for the Echo application.

Before looking at the code, it helps to know what each part is doing. The app defines a bucket and a key that hold shared configuration. Then it defines one handler set for writing configuration and another handler set for reading that configuration during request handling. Finally, simpleApp composes those handlers together with ObjectDB, which provides the shared storage capability underneath.
import { simpleApp } from "distlang/layers";
import { helpers } from "distlang";
const configBucket = "simpleapp";
const echoCountKey = "global-echo-count";
const handlerSet1 = {
routes: {
POST: {
"/echo/config": async ({ req, db }) => {
const body = await req.json();
const requested = Number(body && body.times);
const times = Number.isFinite(requested) && requested > 0 ? Math.floor(requested) : 1;
await db.buckets.create(configBucket);
await db.put(configBucket, echoCountKey, { times });
return Response.json({
ok: true,
configured: { times },
target: "handlerSet1",
});
},
},
},
};
const handlerSet2 = {
routes: {
GET: {
"/echo/:text": async ({ db, params }) => {
const configured = await db.get(configBucket, echoCountKey);
const times = configured && Number.isFinite(Number(configured.times))
? Math.max(1, Math.floor(Number(configured.times)))
: 1;
const textToEcho = params && typeof params.text === "string" ? params.text : "hello";
return new Response(`${Array(times).fill(textToEcho).join("\n")}\n`);
},
},
},
};
export default simpleApp.instantiate(handlerSet1, handlerSet2, helpers.ObjectDB);
There are a few useful things to notice in this example.
handlerSet1 exposes POST /echo/config. That route accepts a JSON payload, reads the times value, normalizes it, and writes it into shared storage. In other words, this part of the application is responsible for configuration.
handlerSet2 exposes GET /echo/:text. That route reads the stored configuration, falls back to a default value when nothing has been configured yet, and uses the result to decide how many times to repeat the requested text. This part of the application is responsible for runtime behavior.
The last line is where the layering becomes explicit:
simpleApp.instantiate(handlerSet1, handlerSet2, helpers.ObjectDB)
This is the composition step. simpleApp takes separate handler sets and combines them around a shared capability. ObjectDB provides the lower-layer storage behavior. simpleApp uses that behavior to define a higher-level application structure.
The curl output makes this easier to see in practice. A request to the configuration endpoint updates the stored repeat count. A request to the echo endpoint then reflects that new value immediately. The two handlers have different responsibilities, but they are still cooperating through the shared layer underneath.
:~/workspace/distlanglabs/distlang$ curl -X POST "http://127.0.0.1:5656/echo/config" -H "Content-Type: application/json" -d '{"times":5}' | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 70 100 59 100 11 23723 4422 --:--:-- --:--:-- --:--:-- 35000
{
"ok": true,
"configured": {
"times": 5
},
"target": "handlerSet1"
}
:~/workspace/distlanglabs/distlang$ curl "http://127.0.0.1:5657/echo/hi"
hi
hi
hi
hi
hi
:~/workspace/distlanglabs/distlang$ curl -X POST "http://127.0.0.1:5656/echo/config" -H "Content-Type: application/json" -d '{"times":2}' | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 70 100 59 100 11 33966 6332 --:--:-- --:--:-- --:--:-- 70000
{
"ok": true,
"configured": {
"times": 2
},
"target": "handlerSet1"
}
:~/workspace/distlanglabs/distlang$ curl "http://127.0.0.1:5657/echo/hi"
hi
hi
:~/workspace/distlanglabs/distlang$
What makes this interesting is that layering does not need to stop at simpleApp.
If ObjectDB is a lower layer that provides storage capability, and simpleApp is a higher layer that defines a reusable application pattern, then more specialized layers can be built on top of that pattern over time.
For example, an AuthorizedSimpleApp could provide the same kind of application structure but include authentication and authorization as part of the layer itself. A future MultiDBApp could coordinate multiple storage systems so a single database is not the only source of truth or the only point of failure. Other layers could introduce reliability policies, metrics, queueing behavior, or workflow-specific conventions.
The goal is not to define every application shape in advance. The goal is to make it possible to build upward in reusable layers, where each layer absorbs one category of complexity and presents a simpler model to the layer above it.
That is the part we are exploring with simpleApp. It is not just a convenience wrapper around handlers. It is an early example of how distlang might let applications grow through layered abstractions: first a capability, then an application pattern, then potentially richer and more opinionated patterns on top of that.
Just as important, this kind of layering should not reduce distlang to a single deployment model or a single provider path. These layers are meant to improve the application model, not narrow the infrastructure choices underneath it.
If good distributed-systems habits can become first-class parts of a layer, then stronger application patterns can become portable too. That could make better defaults around authorization, redundancy, failure handling, and storage coordination available across platforms instead of forcing every application to rebuild those ideas from scratch.