Coming from Spring Boot
Most of what Spring Boot, Actuator, ShedLock and Resilience4j give a service has a grelmicro equivalent. This page maps each one, says where the behavior differs, and lists what grelmicro leaves to other libraries.
Quick reference
| Spring | grelmicro | Page |
|---|---|---|
| Actuator liveness and readiness probes | HealthChecks with /livez, /readyz and /healthz |
Health |
| A health indicator left out of readiness | @health.check(..., critical=False) |
Health |
| A health indicator added to the liveness group | @health.check(..., liveness=True) with HealthChecks(liveness=Liveness(...)) |
Catch a stuck worker |
management.server.port |
OpsServer, for a process that serves no HTTP |
Ops server |
Micrometer, /actuator/prometheus |
Metrics() and metrics_router() |
Metrics |
@Timed |
@measure |
Metrics |
@Scheduled(fixedDelay = ...), @Scheduled(cron = ...) |
@tasks.every(interval=...), @tasks.cron("...") |
Task Scheduler |
ShedLock @SchedulerLock |
gate="claim" on a task |
Task Scheduler |
@Cacheable, @CacheEvict |
@cached(cache, tags=[...]), cache.delete_tags(...) |
@cached |
@Cacheable(sync = true) |
@cached(...), whose default lock="process" folds misses in one process. lock="cluster" folds them across replicas |
Stampede protection |
| Resilience4j CircuitBreaker, Retry, RateLimiter, Bulkhead, TimeLimiter | CircuitBreaker, @retry, RateLimiter, Bulkhead, Timeout |
Resilience |
| SLF4J MDC | add_context() in a handler, @instrument and span() |
Tracing |
| OAuth2 resource server with JWT | AuthenticatedRequests with a JWTVerifier |
Authentication |
hasAuthority("SCOPE_x") |
Authenticated(scopes=["x"]) |
Authentication |
| OAuth2 client credentials | OAuthClient with ClientCredentials |
Tokens |
@TransactionalEventListener |
Outbox.publish(...) and @outbox.handler |
Outbox |
@SpringBootTest with MockMvc, @MockitoBean |
TestClient(app) with micro.fake() |
Testing |
Concept differences
A claimed task renews its lease
ShedLock releases the lock when the task ends, and at the latest after lockAtMostFor. Without its KeepAliveLockProvider, a task that runs longer than lockAtMostFor can run on a second node at the same time. A grelmicro task with gate="claim" renews its lease while the body runs, so lease_duration only bounds how long a crashed worker keeps the claim. A bare async with TaskLock(...) does not renew and behaves like ShedLock: lockAtLeastFor is min_hold_duration and lockAtMostFor is lease_duration.
A claimed cron task takes no lock at all. It records each fire it claims, and a fire missed while every worker was down runs once when a worker comes back.
An interval waits for the previous run
@tasks.every(interval=60) counts from the end of one run to the start of the next, like fixedDelay. There is no fixedRate equivalent: a run that takes 20 seconds starts the next one 80 seconds after it started.
Health has three fixed endpoints, not groups
Actuator groups are named sets you define. grelmicro has three endpoints: /livez reflects the liveness checks, /readyz runs the critical checks, /healthz runs the rest. A check with critical=False stays out of readiness, and its failure keeps the answer at 200. There is no custom group.
Retry needs to know what to retry
Spring Framework 7 @Retryable retries every exception, three retries after the first call. grelmicro @retry requires when=, the exceptions worth retrying, and attempts counts every call, the first one included.
The circuit breaker counts consecutive failures
Resilience4j opens on a failure rate or a slow-call rate over a sliding window. grelmicro opens after a number of consecutive failures. Its state can live in Redis, Valkey or PostgreSQL, so every replica sees the same breaker.
Resilience runs in a fixed order
Resilience4j wraps a call as Retry(CircuitBreaker(RateLimiter(TimeLimiter(Bulkhead(call))))). A grelmicro Stack runs Fallback, Retry, CircuitBreaker, RateLimiter, Bulkhead and then Timeout, so the timeout sits inside the bulkhead.
Context lives on the call, not the thread
MDC is a map for each thread that you can write to anywhere. grelmicro context follows the call across await. micro.install(app) opens one for each request and message, and @instrument and span() open one for a block, so add_context in a route handler adds its fields to every log record until the response. Outside all of them, add_context does nothing.
The outbox survives a crash
@TransactionalEventListener runs after the commit, in memory. A crash between the commit and the listener loses the event. Outbox.publish(...) writes the message in the same transaction, and a relay delivers it at least once, with retries and a dead-letter state.
Side by side
A job that runs on one node
@Scheduled(fixedDelay = 60_000)
@SchedulerLock(name = "cleanup", lockAtMostFor = "5m", lockAtLeastFor = "30s")
public void cleanup() {
// ...
}
from grelmicro.task import Tasks
tasks = Tasks()
@tasks.every(interval=60, gate="claim")
async def cleanup():
print("Running cleanup...")
A check that stays out of readiness
management:
endpoint:
health:
group:
readiness:
include: "readinessState,db"
from grelmicro.health import HealthChecks, HealthDetails
health = HealthChecks()
@health.check("external-api", critical=False)
async def check_external_api() -> HealthDetails | None:
return None
A JWT resource server
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/catalog").permitAll()
.requestMatchers(HttpMethod.DELETE, "/orders/**")
.hasAuthority("SCOPE_orders:write")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
from fastapi import FastAPI
from grelmicro import Grelmicro
from grelmicro.http import AuthenticatedRequests, ErrorResponses
from grelmicro.integrations.fastapi import (
Anonymous,
Authenticated,
CurrentPrincipal,
)
from grelmicro.security import JWTVerifier
micro = Grelmicro(
uses=[
ErrorResponses(),
AuthenticatedRequests(
JWTVerifier.discover(
"https://auth.example.com/", audience="orders-api"
),
exclude=("/livez", "/readyz"),
),
]
)
app = FastAPI()
@app.get("/orders")
async def list_orders(principal: CurrentPrincipal) -> dict[str, str | None]:
return {"caller": principal.subject}
@app.delete(
"/orders/{order_id}",
dependencies=[Authenticated(scopes=["orders:write"])],
)
async def cancel(order_id: int) -> dict[str, int]:
return {"cancelled": order_id}
@app.get("/catalog", dependencies=[Anonymous()])
async def catalog() -> list[str]:
return ["tea", "coffee"]
micro.install(app)
A side effect after the commit
@Transactional
public void signUp(String email) {
User user = users.save(new User(email));
events.publishEvent(new UserSignedUp(user.getId(), email));
}
@TransactionalEventListener
public void sendWelcome(UserSignedUp event) {
// ...
}
import asyncio
import os
from pydantic import BaseModel
from grelmicro import Grelmicro
from grelmicro.outbox import Message, Outbox
from grelmicro.providers.postgres import PostgresProvider
postgres = PostgresProvider(os.environ["POSTGRES_URL"])
outbox = Outbox(postgres)
micro = Grelmicro(uses=[outbox])
delivered = asyncio.Event()
class WelcomeEmail(BaseModel):
to: str
user_id: int
async def send_email(to: str, idempotency_key: str) -> None:
print(f"welcome email to {to}")
delivered.set()
@outbox.handler(WelcomeEmail)
async def send_welcome(message: Message[WelcomeEmail]) -> None:
assert message.data is not None # a typed handler always gets its model
await send_email(to=message.data.to, idempotency_key=str(message.id))
async def sign_up(email: str) -> None:
async with postgres.client.acquire() as conn, conn.transaction():
user_id = await conn.fetchval(
"INSERT INTO users (email) VALUES ($1) RETURNING id", email
)
await outbox.publish(conn, WelcomeEmail(to=email, user_id=user_id))
async def main() -> None:
async with micro:
await postgres.client.execute(
"CREATE TABLE IF NOT EXISTS users (id serial PRIMARY KEY, email text)"
)
await sign_up("alice@example.com")
async with asyncio.timeout(5):
await delivered.wait()
if __name__ == "__main__":
asyncio.run(main())
False friends
| Word | In Spring | In grelmicro |
|---|---|---|
| profile | A named set of configuration, chosen by spring.profiles.active |
A Shield preset such as internal or api. GREL_ENVIRONMENT only names the deployment tier and selects no configuration |
| container | The IoC container that builds and injects beans | The Grelmicro app that opens and closes your components, or a Docker container |
| component | Any bean found by @Component scanning |
One pattern wired into the app, such as Cache or Coordination, with a start and a stop |
| provider | An AuthenticationProvider |
A connection to one vendor, such as RedisProvider |
| tags | Micrometer meter dimensions | Cache tags for bulk invalidation. Metric dimensions are called attributes |
No equivalent, do this instead
- Profiles: set the values for each environment in
GREL_*variables or a mounted file. See Configuration. - Custom health groups: use
critical=Falseand the?exclude=query on/readyzand/healthz. - A failure-rate circuit breaker: grelmicro has the consecutive count only.
- An application event bus: call the function. Use the Outbox for work after a commit, and FastStream for a broker.
- Spring Data: use SQLAlchemy or SQLModel.
Outbox.publishtakes theirAsyncSession. - WebSocket broadcast: use your framework's WebSocket support with a broker. grelmicro still authenticates the handshake.