Skip to main content
ImmutableLog logo
BackJava / JVM

Java

Complete integration guide for ImmutableLog in Java. Choose between automatic middleware for Spring Boot, Quarkus, and Micronaut, direct HTTP client for workers and jobs, or @AuditEvent via Spring AOP to instrument service methods.

Framework filters

The fastest way to integrate is via the framework's native HTTP filter. Every request is automatically captured — latency, status, client IP, and body hash — without modifying any controller. Click the framework to see the complete code.

Direct HTTP client

Use java.net.http.HttpClient (Java 11+) to send events directly to ImmutableLog without depending on a web framework. Ideal for workers, Kafka consumers, scheduled jobs, and any JVM code that needs to record events in the ledger.

java
import java.net.URI;
import java.net.http.*;
import java.util.*;
import com.fasterxml.jackson.databind.ObjectMapper;

public class ImmutableLogClient {

    private static final String API_URL = "https://api.immutablelog.com/v1/events";
    private final String apiKey;
    private final String serviceName;
    private final String env;
    private final HttpClient http = HttpClient.newHttpClient();
    private final ObjectMapper mapper = new ObjectMapper();

    public ImmutableLogClient(String apiKey, String serviceName, String env) {
        this.apiKey = apiKey;
        this.serviceName = serviceName;
        this.env = env;
    }

    public CompletableFuture<Void> sendEvent(
        String eventName,
        String type,          // "success" | "info" | "error"
        Map<String, Object> payload
    ) {
        try {
            String requestId = UUID.randomUUID().toString();

            Map<String, Object> meta = Map.of(
                "type", type,
                "event_name", eventName,
                "service", serviceName,
                "request_id", requestId,
                "env", env
            );

            Map<String, Object> body = Map.of(
                "payload", mapper.writeValueAsString(payload),
                "meta", meta
            );

            String json = mapper.writeValueAsString(body);

            var request = HttpRequest.newBuilder()
                .uri(URI.create(API_URL))
                .header("Content-Type", "application/json")
                .header("Authorization", "Bearer " + apiKey)
                .header("Idempotency-Key", requestId)
                .POST(HttpRequest.BodyPublishers.ofString(json))
                .build();

            // Fire and forget — never block the caller
            return http.sendAsync(request, HttpResponse.BodyHandlers.discarding())
                .thenApply(_ -> null);

        } catch (Exception e) {
            return CompletableFuture.failedFuture(e);
        }
    }
}

// Usage:
var client = new ImmutableLogClient(
    System.getenv("IMTBL_API_KEY"), "my-service", "production"
);

client.sendEvent("user.created", "success", Map.of(
    "user_id", "usr_123",
    "email_hash", sha256("user@example.com"),
    "plan", "pro"
));

sendAsync() returns a CompletableFuture without blocking the thread. The event is sent asynchronously — code execution continues immediately after the call.

Retry with exponential backoff

For high-availability production, add automatic retry with exponential backoff. ImmutableLog returns 202 on success and 429 when the monthly limit is reached — never retry on 429.

java
public CompletableFuture<Void> sendEventWithRetry(
    String eventName,
    String type,
    Map<String, Object> payload,
    int maxAttempts
) {
    return sendWithRetry(eventName, type, payload, maxAttempts, 0, 500);
}

private CompletableFuture<Void> sendWithRetry(
    String eventName, String type, Map<String, Object> payload,
    int maxAttempts, int attempt, long delayMs
) {
    return sendEventRaw(eventName, type, payload)
        .handle((res, ex) -> {
            // 202 = success, 429 = rate limited (never retry)
            if (ex == null) return CompletableFuture.<Void>completedFuture(null);
            if (attempt + 1 >= maxAttempts) return CompletableFuture.<Void>failedFuture(ex);

            return CompletableFuture
                .runAsync(() -> {}, CompletableFuture.delayedExecutor(
                    delayMs, TimeUnit.MILLISECONDS
                ))
                .thenCompose(_ -> sendWithRetry(
                    eventName, type, payload,
                    maxAttempts, attempt + 1, delayMs * 2   // exponential backoff
                ));
        })
        .thenCompose(f -> f);
}

Batch sending

In high-frequency workers, accumulate events and send in parallel using CompletableFuture.allOf(). Each event is sent individually — allOf() parallelizes without failing the entire batch.

java
public CompletableFuture<Void> sendBatch(List<EventDto> events) {
    // Send all events in parallel — fire and forget each one
    var futures = events.stream()
        .map(e -> sendEvent(e.eventName(), e.type(), e.payload())
            .exceptionally(ex -> null)  // never fail the batch
        )
        .toList();

    return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
}

// Usage:
client.sendBatch(List.of(
    new EventDto("order.created", "success", Map.of("order_id", "ord_1")),
    new EventDto("payment.captured", "success", Map.of("amount", 9900)),
    new EventDto("email.sent", "info", Map.of("template", "welcome"))
));

AOP @AuditEvent

With Spring AOP, instrument any service method without modifying business logic. The event is emitted in the finally block — recorded even when the method throws an exception.

java
// 1. Add dependency: spring-boot-starter-aop
// pom.xml: <artifactId>spring-boot-starter-aop</artifactId>

// 2. Define the annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AuditEvent {
    String value() default "";
    String type() default "success";
}

// 3. Implement the aspect
@Aspect
@Component
public class AuditEventAspect {

    private final ImmutableLogClient client;

    public AuditEventAspect(ImmutableLogClient client) {
        this.client = client;
    }

    @Around("@annotation(auditEvent)")
    public Object audit(ProceedingJoinPoint pjp, AuditEvent auditEvent)
        throws Throwable {
        long startMs = System.currentTimeMillis();
        String name = auditEvent.value().isBlank()
            ? pjp.getSignature().getDeclaringTypeName() + "." + pjp.getSignature().getName()
            : auditEvent.value();

        try {
            Object result = pjp.proceed();
            long latency = System.currentTimeMillis() - startMs;
            client.sendEvent(name, auditEvent.type(), Map.of("latency_ms", latency));
            return result;
        } catch (Throwable ex) {
            long latency = System.currentTimeMillis() - startMs;
            client.sendEvent(name, "error", Map.of(
                "latency_ms", latency,
                "error_type", ex.getClass().getName(),
                "error_message", ex.getMessage()
            ));
            throw ex;
        }
    }
}

// 4. Use on any Spring bean:
@Service
public class OrderService {

    @AuditEvent("order.processed")
    public Order process(OrderRequest req) {
        // ... business logic
    }

    @AuditEvent(value = "payment.captured", type = "success")
    public void capturePayment(String orderId, long amountCents) {
        // ...
    }
}

@Around captures exceptions

@Around is used instead of @AfterReturning/@AfterThrowing to ensure the event is emitted in all cases — success, failure, or exception.

Auto name via reflection

If value() is empty, the aspect uses the method name via pjp.getSignature(). Always prefer an explicit semantic name via @AuditEvent("domain.action").

Spring Bean configuration

For Spring Boot projects, register ImmutableLogClient as a @Bean for injection via @Autowired or constructor in any component.

java
// Register ImmutableLogClient as a Spring bean for injection
@Configuration
public class ImmutableLogConfig {

    @Bean
    public ImmutableLogClient immutableLogClient(
        @Value("${immutablelog.api-key}") String apiKey,
        @Value("${immutablelog.service-name:my-service}") String service,
        @Value("${immutablelog.env:production}") String env
    ) {
        return new ImmutableLogClient(apiKey, service, env);
    }
}

// application.properties:
// immutablelog.api-key=${IMTBL_API_KEY}
// immutablelog.service-name=my-service
// immutablelog.env=production

This documentation reflects the current API behavior. For questions or advanced integrations, contact the support team.