HTTP Server For JSON Event Sourcing

May 5, 2026 ยท View on GitHub

This is a standalone HTTP server for interacting with JSON aggregates. On the write-side you can send commands, which are handled asynchronously. They are put on a Kafka command topic, which corresponds to the aggregate type in the command. This is acknowledged with a 202 HTTP status code (Accepted). Changes to aggregates come back through Server-Sent Events. This flow fits well with reactive clients.

The read-side is handled with MongoDB. You can fetch and search aggregates.

The supported paths and methods are explained in the repository pincette-jes-api.

One special path is <contextPath>/health, which just returns status code 200 (OK). This can be used for health checks.

Authentication

All requests should have a JSON Web Token, which may appear as a bearer token in the Authotrization header or the cookie named access_token. If the configuration has a public key, then the tokens will be validated. But normally you would put the service behind some gateway that handles the validation.

Authorization

The roles field in the JWT is used to determine whether the request is allowed. It is an array of strings. A command with access control has an array of roles that can execute the command. If the intersection of the latter and the roles field is not empty, the request can go through.

In the configuration you can have entries like authorization.<aggregate>.<command>, which have arrays of role names as their value. These are used for the intersection. If there are no roles for a command, then the configuration entry authorization.denyByDefault says whether the request is allowed.

The HTTP methods GET, PUT and DELETE correspond respectively to the commands get, put and delete. Posting a search to an aggregate also corresponds to the get command. All other commands are posted to an individual aggregate instance.

If the data set for an aggregate is segmented across the population that can work with it, then you can use Access Control Lists in the data itself. So, you could have some high-level role in the configuration and more specific ones in the ACLs.

The ACLs also filter what is returned to the client. A search result will only contain the aggregate instances the user is allowed to see. A GET on an instance the user cannot see will return 404. So, ACLs completely hide aggregate instances.

Configuration

The configuration is managed by the Lightbend Config package. By default, it will try to load conf/application.conf. An alternative configuration may be loaded by adding -Dconfig.resource=myconfig.conf, where the file is also supposed to be in the conf directory. If no configuration file is available it will load a default one from the resources. The following entries are available:

EntryDefaultDescription
accessLogfalseA boolean indicating if access log entries should be sent to the log topic, which should be set.
authorization.denyByDefaultfalseCommands for which there are no roles are allowed if this entry is false.
authorization.<aggregate>.<command>NoneThe array of role names that are allowed to execute the command.
contextPath/apiThe URL path prefix.
environmentNoneThe name of the environment, which will be used as a suffix for the aggregates, e.g. tst, acc, etc.
jwtPublicKeyNoneThe public key string, which is used to validate all JSON Web Tokens.
kafkalocalhost:9092All Kafka settings come below this entry. So for example, the setting bootstrap.servers would go to the entry kafka.bootstrap.servers. The equivalent environment variable would then be KAFKA_BOOTSTRAP_SERVERS.
logLevelINFOThe log level as defined in java.util.logging.Level.
mongodb.databaseesThe name of the MongoDB database.
mongodb.urimongodb://localhost:27017The URI of the MongoDB service.
namespacejes-httpA name to distinguish several deployments in the same environment.
otlp.grpcNoneThe OpenTelemetry endpoint for logs and metrics. It should be a URL like http://localhost:4317.
slowRequestThresholdNoneIf this duration is set, then requests that take longer are logged with their request body.
traceSamplePercentage10The percentage of distributed trace samples that are retained. The value should be between 1 and 100. You should use the same percentage in all components that contribute to a trace, otherwise you may see incomplete traces.
tracesTopicNoneThe Kafka topic to which event traces are sent.
whoamiNoneAn array of fields that are extracted from the JWT and put in a JSON object that becomes the value of the whoami cookie. The cookie can be used by clients to obtain basic information about the current user.

Telemetry

A few OpenTelemetry observable counters are emitted every minute. The following table shows the counters.

CounterDescription
http.server.average_duration_millisThe average request duration in the measured interval.
http.server.average_request_bytesThe average request body size in bytes in the measured interval.
http.server.average_response_bytesThe average response body size in bytes in the measured interval.
http.server.requestsThe number of requests during the measured interval.

The following attributes are added to the counters.

AttributeDescription
aggregateThe name of the aggregate the request was about.
http.request.methodThe request method.
http.response.status_codeThe status code of the response.
instanceThe UUID of the JES HTTP instance.

The logs are also sent to the OpenTelemetry endpoint.

The event traces are JSON messages, as described in JSON Streams Telemetry. They are sent to the Kafka topic set in the tracesTopic configuration field.

Building and Running

You can build the tool with mvn clean package. This will produce a self-contained JAR-file in the target directory with the form pincette-jes-http-<version>-jar-with-dependencies.jar. You can launch this JAR with java -jar, followed by a port number.

You can run the JVM with the option -mx128m.

Docker

Docker images can be found at https://hub.docker.com/repository/docker/jsoneventsourcing/pincette-jes-http. They expose port 9000. You can either use environment variables to configure them or add a configuration layer with a Docker file that looks like this:

FROM registry.hub.docker.com/jsoneventsourcing/pincette-jes-http:<version>
COPY conf/tst.conf /conf/application.conf

So wherever your configuration file comes from, it should always end up at /conf/application.conf.

Kubernetes

You can mount the configuration in a ConfigMap and Secret combination. The ConfigMap should be mounted at /conf/application.conf. You then include the secret in the configuration from where you have mounted it. See also https://github.com/lightbend/config/blob/main/HOCON.md#include-syntax.