A pure-Java engine for managing leasable resources that carry a reputation — proxy endpoints, external accounts, browser sessions: anything you borrow, that degrades when it fails, cools down, and recovers.
The core makes one decision — "may I lend this resource, for this context, right now?" — as a pure function over immutable state. It has zero runtime dependencies (JDK only): no Spring, no Netty, no database, no network. Time, storage, health-probing, and observability are pushed behind four interfaces (ports), so the same engine embeds as a library, fronts a gRPC service, or runs as a gateway — unchanged.
Status — early development.
reputation-pool-core(the pure decision engine) is published to Maven Central;0.2.0— the second Central release, adding the snapshot/persistence surface — is documented in the changelog. The L1 adapters, the L2 gRPC advisor, and L3 persistence (snapshot + audit trail) are done. Layers are added as separate modules in this same repository — see the roadmap and Design notes for the full target architecture.
Reputation bugs leak slowly — a proxy that gets blocked on one platform quietly poisons the pool for every platform, and logs don't catch it. This engine addresses that with three structural choices:
- Decisions are pure functions.
apply(cell, outcome, now) → next cellhas no side effects, so property tests can attack its invariants over thousands of generated outcome sequences, and a production incident reproduces by replaying the same inputs. - State is immutable and concurrency lives in the data. A
ReputationCellis an immutable record; updating it swaps a single reference, so readers never observe a torn state and atomicity comes fromConcurrentHashMap.compute()instead of distributed locks. - Purity is enforced by the build, not by discipline. An ArchUnit rule rejects any
core → Spring / Netty / JDBC / gRPCimport as a build failure, so the dependency-free boundary is a fact CI guards, not a promise.
- Java 25+ — the first LTS where virtual threads no longer pin the carrier thread inside
synchronized(JEP 491), which the health-prober layer relies on.
Available on Maven Central. Requires JDK 25+. Or build from source (
./gradlew build).
// build.gradle.kts
dependencies {
implementation("io.github.preagile:reputation-pool-core:0.2.0")
}A minimal embed — the whole M1 API is three calls:
// windowSize 10, cool after 3 consecutive failures, promote back to HEALTHY
// after 2 consecutive post-cooldown successes
ReputationEngine engine = new ReputationEngine(new AdaptiveCooldownPolicy(), 10, 3, 2);
ReputationCell cell = ReputationCell.fresh(
new ResourceId(ResourceKind.PROXY, "10.0.0.7:8080"),
new Context("marketplace-a"),
clock.instant()); // inject java.time.Clock — core never reads the wall clock itself
// report each use; apply is pure: (cell, outcome, now) -> next cell + events
ReputationEngine.Result result = engine.apply(
cell, new Outcome.Failure(FailureType.TIMEOUT, Duration.ofSeconds(2)), clock.instant());
cell = result.cell();
result.events().forEach(this::publish); // ResourceCooled, ResourceRecovered, ...| Concept | What it is |
|---|---|
| Resource | Something you lease and that carries reputation — a proxy endpoint, an account, a session. |
| Context | The scope a reputation applies to (e.g. a platform). Failures in one context never affect another. |
| Outcome | The result of one use: Success(latency) or Failure(type, latency). The engine's only input. |
| ReputationCell | One (resource × context) cell — score, consecutive failures, recent window, state, cooldown. Immutable. |
| State | HEALTHY → COOLING → RECOVERING → HEALTHY, plus BLOCKLISTED. Decides selectability. |
Effective score is two-layered: effective = globalBase(resource) + contextDelta(resource, context) — a
shared per-resource signal plus a per-context behavioural signal, so a block on one platform doesn't sink the
resource everywhere.
| Module | Description | Status |
|---|---|---|
reputation-pool-core |
Pure decision engine — domain, engine, ports. JDK only. | Done |
reputation-pool-adapters |
Demo resource kinds (proxy, account) implementing the ports. | Done |
reputation-pool-persistence |
PostgreSQL adapter — snapshot store + append-only audit trail (plain JDBC + Flyway). | Done |
reputation-pool-server |
gRPC advisor (L2) + durable lifecycle wiring (L3); virtual-thread probing and observability later. | In progress |
The server runs in-memory by default; setting REPUTATION_POOL_DB_URL (plus
REPUTATION_POOL_DB_USERNAME / REPUTATION_POOL_DB_PASSWORD) switches it to persistent mode — Flyway
migrates the schema, the pool restores from the last snapshot, and every pool event is appended to the
audit trail.
The audit trail is append-only and grows without bound unless you opt in to retention:
REPUTATION_POOL_AUDIT_RETENTION takes an ISO-8601 duration (e.g. P30D for thirty days) and turns on
an hourly background purge of events older than that — the purge only ever trims the oldest tail of the
history, never rewrites what survives. The bound is honest at the margin: events are purged once they
are older than the retention and no younger-stamped event precedes them in the trail, so the
effective upper bound is the retention plus at most the emitters' timestamp skew and one purge period.
Unset means never purge, exactly the pre-knob behavior.
The repository is one hexagon that grows outward from a pure core, and every dependency points inward:
an ArchUnit rule fails the build on any core → Spring / Netty / JDBC / gRPC import, so the dependency-free
boundary is guarded by CI, not by convention.
flowchart TB
adapters["reputation-pool-adapters — L1 (done)<br/>proxy and account demos"]
persistence["reputation-pool-persistence — L3 (done)<br/>snapshot store + audit trail"]
server["reputation-pool-server — L2 · L3 (done)<br/>gRPC advisor · durable lifecycle"]
subgraph core["reputation-pool-core — pure Java, JDK only"]
M1["M1 (done) · decision engine<br/>pure (state, outcome, now) → new state"]
M2["M2 (done) · concurrency layer<br/>ResourcePool facade + ports"]
end
adapters -->|depends on| core
persistence -->|depends on| core
server -->|depends on| core
The roadmap labels encode that boundary:
- M — milestones build the pure core itself (
reputation-pool-core: JDK-only, no I/O).M1is the decision engine;M2is the concurrency layer and the first port. - L — layers are separate modules added around the core, where frameworks and real I/O are allowed.
L1is the demo adapters,L2the gRPC server,L3persistence.
The line between them is exactly the module boundary the ArchUnit rule protects: code inside core stays a
pure function of its inputs, and everything that touches the network, a clock, or a database lives in an
L module that depends inward on core — never the other way around.
New to the project? Concepts, flow, and vocabulary walks through who-calls-whom and every term (lease, advisory, fencing token, EventSink, advisor, ...) in plain language.
./gradlew buildbuild runs the full gate: Spotless formatting check, unit + property (jqwik) + concurrency tests, and the
ArchUnit purity rules. The build provisions JDK 25 automatically via the Foojay toolchain resolver.
- M1 — core decision engine: domain records,
ReputationEngine,AdaptiveCooldownPolicy; jqwik invariants + ArchUnit purity gate. - M2 — concurrency layer:
Blocklist,SelectionStrategy,LeaseRegistry,ResourcePoolfacade, and the firstEventSinkport; 32-thread lease-exclusivity tests. - L1 — adapter demos: proxy and account adapters driven by the same engine — per-kind outcome classifiers and WireMock end-to-end cooling/recovery tests.
- L2 — gRPC advisor:
acquire / report / renew / release+ event stream; publish core to Maven Central. - L3 — persistence: whole-pool snapshot behind the
ResourceStoreport, plus an append-only audit trail as a secondEventSinkimplementation — the two persistence shapes deliberately sit behind different ports (replace-latest vs. append-history).
The full target architecture (state machine, two-layer reputation model, gRPC contract, storage design, SLOs)
lives in the design documents. This repository grows outward from core; each layer is optional and the story
is complete at every stopping point.
Licensed under the Apache License 2.0.