krizaka-platform-kit
Platform kit
The cross-cutting code every Spring Boot service writes — written once, with its invariants tested.
The problem it removes
An event published after the commit is lost on the next crash.
Publishing an event right after the database write: on a crash between the two, the event is lost; send it first and a rollback has already told the world. A failing @RabbitListener requeued forever blocks its queue. Every service invents its own error JSON. Across Orazaka's services this code existed five to seven times — with three live bugs between the copies.
- Dual write: a service that writes its row then calls
rabbitTemplate.convertAndSendloses the event when it crashes in between, and one that sends first announces changes that roll back. - Poison messages: a listener that throws on a malformed message is redelivered at once, forever, and nothing else in the queue moves.
- Deduplication done by hand: a "does it exist? then insert" lets two overlapping deliveries both through — exactly when a broker redelivers a slow first attempt — and a claim kept after a failure silently drops the retry.
- Security chains copied per service: two of six had drifted in Orazaka; one answered a CORS preflight with 401.
What it does
- Events go through a transactional outbox: they commit or roll back with your data.
- Queues retry 5 times, then dead-letter to
.dlq— a poison message never loops. - One security baseline and RFC 9457 errors with a
requestId, in every service.
Four modules and four starters: one security baseline for every filter chain, events through a transactional outbox, consumer queues that retry then dead-letter, idempotent consumption, RFC 9457 errors with a request id, and required service names on every metric.
Nothing is open by omission.
SecurityBaselineends every chain withanyRequest().authenticated(): the path you forgot to list is closed, not public.
Decisions and trade-offs
We chose
A transactional outbox:
EventPublisher.publishwrites a row of your outbox in your transaction; a relay claims rows withFOR UPDATE SKIP LOCKEDand publishes them, with amessageIdchosen at write time.We refused
Publishing to the broker from the service method, and a distributed transaction (XA) across database and broker.
Because
The event commits or rolls back with the change. A relay that crashes republishes the same
messageId, and the consumer's dedup makes it a no-op.What it costs you
An outbox table per context (you implement
OutboxStoreover your schema) and a polling delay between commit and publish.We chose
The envelope in AMQP headers (
kz-type,kz-version,kz-producer,kz-correlation-id,kz-occurred-at), the body the bare event, and a JSON Schema per event in the producer's-apimodule.We refused
A shared jar of event DTOs, and a JSON wrapper around every body.
Because
A shared DTO jar ties every service's release to every other's. A schema checked by tests on both sides (
EventContractTest) gives the contract without the coupling, and the body stays readable by any language.What it costs you
Each consumer keeps its own tolerant copy of the event and one contract test.
We chose
Quorum queues declared in one line (
KrizakaQueues.consumer), a stateless retry with back-off — 5 attempts from 500 ms — then the queue’s.dlqtwin with the original headers and the exception.We refused
Spring's default requeue on failure.
Because
A requeued poison message loops forever; in the DLQ it is visible, with the reason, and can be replayed by hand.
What it costs you
A queue already declared as classic must be drained and redeclared once.
We chose
RFC 9457 Problem Details for every error: a
DomainExceptionbecomes its 4xx with a stable code and therequestId; bean validation a 422 witherrors[]; anything unexpected a 500 that never carries its message.We refused
Free-form error bodies per service, and stack traces or exception messages in 500 responses.
Because
A client handles one shape, and the
requestIdin the response is the one in the log line — a support ticket points at the failure.What it costs you
Your exceptions extend
DomainException(NotFoundException,ConflictException…) to choose their status.We chose
Starters that are POMs only, over modules usable alone; defaults applied at the lowest priority.
We refused
Renaming the modules into starters, and defaults that override
application.yml.Because
A product declares capabilities, not the Spring list behind them; a Spring Boot upgrade changes the starters, not your POM — and your
application.ymlalways wins.What it costs you
Java 21 and Spring Boot 4.0 only.
In code
@Transactional
public User register(NewUser input) {
User user = users.save(input);
// A row of YOUR outbox, in THIS transaction: no event for a rollback, no lost event on a crash.
events.publish("evt.user.registered", 1, new UserRegistered(user.id(), user.email()));
return user;
}
@Bean // the quorum queue, its <queue>.dlq, the bindings
Declarables userEvents(MessagingExchanges exchanges) {
return KrizakaQueues.consumer(exchanges, "krizaka.notifications.user-events", "evt.user.registered");
}
@RabbitListener(queues = "krizaka.notifications.user-events") // 5 retries with back-off, then the DLQ
void onRegistered(UserRegistered event, @Header(AmqpHeaders.MESSAGE_ID) String messageId) {
if (!dedup.claim("notifications.user-events", messageId)) return; // a redelivery: already done
try { welcome(event); }
catch (RuntimeException e) { dedup.release("notifications.user-events", messageId); throw e; }
}Don't use it when
- Your services run on Spring Boot 3 or Java 17 — the kit targets Spring Boot 4.0 and Java 21.
- Your HTTP layer is WebFlux:
krizaka-webconfigures servlet applications only. - Your bus is Kafka:
krizaka-messagingis RabbitMQ (AMQP 0-9-1) only. - Your tokens come from an external identity provider signing with RS256 or ES256:
krizaka-securityverifies HS256 today.
Where it stands
0.2.0 on Maven Central (2026-10-10): the four modules and the four starters, built and tested on real PostgreSQL and RabbitMQ in CI.
Published
com.krizaka:krizaka-spring-boot-starter-* 0.2.0 · Maven Centralcom.krizaka:krizaka-web · -security · -messaging · -observability 0.2.0 · Maven CentralIn progress
MessageDedup.processOnce— claim, run, give the claim back on failure. Five listeners in notifications and billing repeat those eight lines today; one of them documents the forgotten release as "the settlement is lost forever". Lands in 0.3.0.krizaka-platform-kit#12- One shared HS256 secret signs and verifies: a service that can verify can mint. Decided: issuer-signed session tokens (JWKS) and per-caller service tokens, HS256 deprecated over one minor.krizaka-platform-kit#11
Adopt it
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.krizaka</groupId>
<artifactId>krizaka-bom</artifactId>
<version>0.2.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency><groupId>com.krizaka</groupId><artifactId>krizaka-spring-boot-starter-web</artifactId></dependency>
<dependency><groupId>com.krizaka</groupId><artifactId>krizaka-spring-boot-starter-security</artifactId></dependency>
<dependency><groupId>com.krizaka</groupId><artifactId>krizaka-spring-boot-starter-rabbitmq</artifactId></dependency>
<dependency><groupId>com.krizaka</groupId><artifactId>krizaka-spring-boot-starter-observability</artifactId></dependency>
</dependencies>Tell us where it hurts.
A block is right when it survives your code base, not ours. Ask in the block's thread, propose a change as an idea, or report a bug on its repository — every decision on this page is open to a better argument.
The other blocks
- UsersSign-up looks like a weekend. Reset tokens, OAuth and JWTs make it a quarter.
- NotificationsAn e-mail sent inside a transaction that rolls back can't be unsent.
- Billing & creditsDebit after and the work ran on credit nobody had. Debit before and failures get billed.
- Build, BOM & test kitParent POMs drift — and a Spring BOM silently overrides the Boot version you chose.
- Krizaka UIThree products, three token vocabularies, 1,153
light:overrides in one app.