Sign inSign up

maifeeulasad/fluid-builder

By maifeeulasad

โ€ขUpdated 9 days ago

Java 24 microservice framework for Kafka. No build tool: annotate a method, ship your .java files.

Image
Developer tools
0

2.5K

maifeeulasad/fluid-builder repository overview

โ ๐ŸŒŠ Fluid

Fluid Builder CI Push to Dockerhub GitHub release Docker image version Docker pulls Docker image size Attestations Dependabot License: MIT

๐Ÿš€ A tiny but agile microservice framework built in Java 24 with first-class support for Docker ๐Ÿณ, Kubernetes โ˜ธ๏ธ, and Kafka ๐Ÿ“จ event streaming. Built for speed, scale, and simplicity.


โ โœจ Features

  • โœ… Java 24-powered lightweight core
  • โœ… ๐Ÿ” Kafka-based event-driven architecture
  • โœ… ๐Ÿณ Docker-ready containers
  • โœ… โ˜ธ๏ธ Kubernetes-deployable out of the box
  • โœ… ๐Ÿ” Minimal boilerplate, maximum flexibility
  • โœ… ๐Ÿ”ง DIY microservice stack for builders and hackers
  • โœ… ๐Ÿ˜ 100% open source

No build tool. There is no Maven or Gradle step to ship a service. Your .java files are copied into the image and compiled at container start.


โ ๐Ÿ“š Libraries

โ Bundled by default

Fetched automatically at container start โ€” nothing to add to your pom.xml:

LibraryUsed for
kafka-clientsโ Kafka producer/consumer
gsonโ JSON (de)serialization
slf4j-apiโ  + slf4j-simpleLogging
spotbugs-annotationsโ Static-analysis annotations
โ Fetched on demand

One environment variable, no pom.xml change: FLUID_METRICS=prometheus pulls in the Prometheus client (see ๐Ÿ“Š Metrics, below); selecting snappy, lz4 or zstd compression needs its own jar too (see โš™๏ธ Configuration below).

โ Supported via your own pom.xml

Fluid has no opinion on how a service talks to a database โ€” add the driver or ORM you want and it is fetched the same way everything else is. Demonstrated end to end, with Docker Compose and CI, in fluid-exampleโ :

BackendWith an ORMWithout
PostgreSQLHibernateraw JDBC
MongoDBโ€”official sync driver
H2 (embedded)โ€”raw JDBC

โ ๐Ÿ“ฆ Getting Started

A service is a Dockerfile, a pom.xml listing your dependencies, and your Java files. Nothing else.

FROM maifeeulasad/fluid-builder:latest

COPY pom.xml .
COPY *.java .

At container start Fluid downloads the dependencies your pom.xml names, compiles your sources, finds your listeners, and starts consuming.

โ Write a listener ๐ŸŽง

Any class with an annotated method is picked up โ€” the file name does not matter.

public class MessageService {

    @KafkaListener(topic = "orders", groupId = "order-processors")
    public void handleOrder(String message) {
        System.out.println("๐Ÿ“ฅ " + message);
    }
}
โ Send messages ๐Ÿ“ค

To write a producer instead of a consumer, supply your own Fluid.java. It replaces the framework's entry point.

public class Fluid {
    public static void main(String[] args) throws InterruptedException {
        try {
            for (int i = 0; i < 1000; i++) {
                KafkaMessenger.sendMessage("orders", "Message " + i);
            }
        } finally {
            KafkaMessenger.shutdown();
        }
    }
}

โ ๐Ÿงฉ Annotations

โ @KafkaListener โ€” the common case
@KafkaListener(topic = "orders", groupId = "order-processors")
public void handleOrder(String message) { }
AttributeDefault
topicrequiredtopic to consume
groupIdrequiredconsumer group to join
bootstrapServersinheritsbroker address; blank follows BOOTSTRAP_SERVERS
โ @KafkaSubscription โ€” full control

Same job, with batching, dead letter routing, topic creation and consumer tuning. Use it when you need one of those; @KafkaListener otherwise.

@KafkaSubscription(
    topic = "orders",
    groupId = "order-processors",
    partitions = 5,
    batchEnabled = true,
    enableDeadLetterQueue = true,
    deadLetterTopic = "order-errors",
    maxPollRecords = 100
)
public void handleOrder(String message) { }

Some attributes are accepted but not yet honoured โ€” each is marked not yet honoured in its javadoc, so a setting that does nothing says so rather than pretending.

A method may carry @KafkaListener or @KafkaSubscription, not both: each drives its own consumer, so the handler would run twice per record. Startup rejects it.

โ @SendTo โ€” forward the result
@KafkaListener(topic = "orders", groupId = "order-processors")
@SendTo(topic = "processed-orders")
public String handleOrder(String message) {
    return "Processed: " + message;
}
โ @ShortCircuit โ€” route failures
@KafkaListener(topic = "orders", groupId = "order-processors")
@ShortCircuit(topic = "order-errors")
public void handleOrder(String message) {
    if (message.contains("fail")) {
        throw new RuntimeException("bad message");
    }
}

โ โš™๏ธ Configuration

VariableDefault
BOOTSTRAP_SERVERSkafka:9092broker address for everything that does not set one explicitly
KAFKA_COMPRESSION_TYPEgzipproducer codec
FLUID_METRICSunset (off)metrics backend โ€” currently prometheus
FLUID_METRICS_PORT9400scrape endpoint port
FLUID_METRICS_PATH/metricsscrape endpoint path
FLUID_METRICS_PUSHGATEWAYunsetPushgateway address, e.g. pushgateway:9091
FLUID_METRICS_JOBhostnamejob label on pushed metrics

gzip is the default because it is the only compressing codec that works with the dependency set Fluid downloads. snappy, lz4 and zstd need third-party libraries that kafka-clients does not bundle โ€” select one and Fluid tells you which library to add rather than failing later at send time.


โ ๐Ÿ“Š Metrics

Off by default. Set one variable and the backend's jars are fetched at container start โ€” nothing to add to your pom.xml, and an image that does not use metrics stays exactly as small as before.

environment:
  - FLUID_METRICS=prometheus

Then scrape http://your-service:9400/metrics.

Everything is named fluid_*, and the prefix is enforced rather than merely documented:

fluid_messages_consumed_total{topic,group,handler}
fluid_messages_produced_total{topic}
fluid_handler_duration_seconds{handler}
fluid_handler_failures_total{handler,topic}
fluid_partition_rewinds_total{topic,partition}
โ Producers that exit

A pull-based scrape cannot observe a process that has already stopped. A batch producer that sends and exits may never be scraped at all.

Point it at a Pushgateway and it pushes on shutdown, whatever entry point it uses:

environment:
  - FLUID_METRICS=prometheus
  - FLUID_METRICS_PUSHGATEWAY=pushgateway:9091

Scrape the gateway with honor_labels: true, or Prometheus rewrites every pushed series to the gateway itself and all your producers collapse into one.

โ Routes you can take, and routes you have taken

At start-up Fluid works out every route your service can take, before it takes any โ€” from @KafkaListener, @SendTo and @ShortCircuit, and by parsing your sources for direct KafkaMessenger.sendMessage("topic", ...) calls.

fluid_route_declared{from,to,type,dynamic}          1 for every possible route
fluid_route_traversed_total{from,to,type,dynamic}   times it actually carried a message

A route with declared=1 and no traversals is a path that exists but has never been used. A topic computed at runtime cannot be known in advance, so it is marked dynamic="true" and appears only once used โ€” which is why the two cases are distinguishable rather than both looking like absence.


โ ๐Ÿ› ๏ธ Architecture

[Your Producer] โ”€โ”€โ–ถ KafkaMessenger โ”€โ”€โ–ถ [Kafka Broker] โ”€โ”€โ–ถ MessageConsumer โ”€โ”€โ–ถ [Your Listener]
                    MessageProducer                       KafkaProcessor
  • ๐Ÿงฉ Handlers are found by annotation at startup, not by file name
  • ๐Ÿงต Records are handled on the poll thread, so per-partition order holds
  • โœ… Offsets are committed only after a record has been processed
  • ๐Ÿ›‘ A failing handler leaves its partition uncommitted and rewinds to retry, rather than committing past the failure

โ ๐Ÿงช Building and testing

Run it the way it ships:

java DependencyDownloader.java   # populates lib/
java -cp "lib/*" Fluid.java

Run the tests:

mvn test

Maven exists for tests, linting and CI only โ€” its output is never shipped, and it compiles the same flat sources in place. See DEVELOPMENT.md.


โ ๐Ÿ” Image attestations

Every tag published to Docker Hub (latest, the date tag, and the commit tag) carries build provenance and an SBOM, attached as attestation manifests alongside the image rather than documented separately โ€” so they can't silently go stale. The publish workflow refuses to push an image that fails its own smoke test first.

Verify either with Buildx, no pull required:

docker buildx imagetools inspect maifeeulasad/fluid-builder:latest

or inspect the SBOM's actual package list with Docker Scoutโ :

docker scout sbom maifeeulasad/fluid-builder:latest

โ ๐Ÿ“š Examples

Runnable, docker-composed examples โ€” Kafka producers/consumers, and database read/write with and without an ORM โ€” live in fluid-ecosystem/fluid-exampleโ , built against this image.


โ ๐Ÿ”ฎ Roadmap

  • ๐Ÿ›‘ Graceful shutdown hooks
  • ๐Ÿ” At-least-once delivery with per-partition ordering
  • ๐Ÿ’€ Dead letter routing
  • ๐Ÿ“Š Metrics (Prometheus), pull and Pushgateway, declared vs. traversed routes
  • ๐Ÿ—บ๏ธ Service topology visualization โ€” see dashboardโ 
  • ๐Ÿ“š Example suite: Kafka (basic + full API), Postgres (JDBC + Hibernate), MongoDB, H2 โ€” see fluid-exampleโ 
  • ๐Ÿ’พ Configuration via fluid.yaml
  • ๐Ÿง  Built-in retry and backoff strategy โ€” @KafkaSubscription accepts the attributes, not yet honoured
  • ๐Ÿ”€ Parallel handling that preserves per-partition order โ€” preserveOrder, not yet honoured

โ ๐Ÿค Contributing

PRs are welcome! Open an issue or suggest an improvement โ€” let's make microservices fun and fast again ๐Ÿงช


โ ๐Ÿ“œ License

MIT License ยฉ 2026 Maifee Ul Asad

Tag summary

Content type

Image

Digest

sha256:cf5b8c468โ€ฆ

Size

501.5 MB

Last updated

9 days ago

docker pull maifeeulasad/fluid-builder