Build distributed services with typed interfaces and declarative resources.
A slice is a service — a typed Java interface with a factory method for dependencies, deployed and managed by the Aether runtime. If you've written a Spring @Service, you already know the programming model.
| You know this... | Slice equivalent |
|---|---|
@Service class | @Slice interface |
@Autowired / constructor injection | Factory method parameters |
| Service method call | Promise<T> method call (local or remote, transparent) |
@RestController | routes.toml (declarative HTTP routing) |
| Kubernetes Deployment YAML | Blueprint TOML |
| HPA (Horizontal Pod Autoscaler) | Built-in reactive + predictive scaling |
There is no message passing, no mailbox, no behavior switching, no supervision tree. Your existing service-based designs, API contracts, domain decomposition, and interaction patterns transfer directly.
@Slice
public interface OrderService {
Promise<OrderResult> placeOrder(PlaceOrderRequest request);
static OrderService orderService(InventoryService inventory) {
record orderService(InventoryService inventory) implements OrderService {
@Override
public Promise<OrderResult> placeOrder(PlaceOrderRequest request) {
return inventory.reserve(new ReserveRequest(request.items()))
.map(reserved -> new OrderResult(reserved.orderId()));
}
}
return new orderService(inventory);
}
}
Aether separates two concerns that traditional frameworks conflate through dependency injection:
Application assembly — wiring internal components together — is fully automated. If a use case depends on a repository, and both are part of the application, the wiring is deterministic. The annotation processor resolves it at compile time. No configuration needed.
Resource provisioning — providing access to external infrastructure — is handled by the runtime. The application declares what it needs through annotated factory method parameters. The runtime decides how to provide it based on the deployment environment.
The annotation processor classifies factory parameters automatically:
| Parameter Pattern | Classification | What Happens |
|---|---|---|
@Sql SqlConnector db | Resource dependency | Provisioned from aether.toml |
InventoryService inventory | Slice dependency | Proxy generated for remote calls |
OrderValidator validator | Plain interface | Factory method called directly |
This separation has immediate consequences:
pom.xml contains only business dependencies. Infrastructure dependencies belong to the runtime.Aether ships four built-in resource qualifiers:
| Annotation | Resource Type | Config Section |
|---|---|---|
@PgSql | PgSqlConnector | "database" |
@Sql | SqlConnector | "database" |
@Http | HttpClient | "http" |
@Notify | NotificationSender | "notification" |
When you need multiple databases or want descriptive naming, create custom qualifier annotations:
@ResourceQualifier(type = SqlConnector.class, config = "database.orders")
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@interface OrderDb {}
@ResourceQualifier(type = SqlConnector.class, config = "database.analytics")
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@interface AnalyticsDb {}
Use them on factory parameters:
@Slice
public interface OrderAnalytics {
Promise<Report> generateReport(ReportRequest request);
static OrderAnalytics orderAnalytics(@OrderDb SqlConnector orders,
@AnalyticsDb SqlConnector analytics) {
record orderAnalytics(SqlConnector orders,
SqlConnector analytics) implements OrderAnalytics {
@Override
public Promise<Report> generateReport(ReportRequest request) {
return Promise.all(
orders.queryOne("SELECT count(*) FROM orders WHERE period = ?",
COUNT_MAPPER, request.period()),
analytics.queryOne("SELECT sum(revenue) FROM sales WHERE period = ?",
SUM_MAPPER, request.period())
).map(Report::new);
}
}
return new orderAnalytics(orders, analytics);
}
}
Each qualifier maps to a separate configuration section in aether.toml:
[database.orders]
jdbc_url = "jdbc:postgresql://localhost:5432/orders"
username = "app"
password = "${secrets:database/orders/password}"
[database.analytics]
jdbc_url = "jdbc:postgresql://localhost:5432/analytics"
username = "app"
password = "${secrets:database/analytics/password}"
Notice: the application never sees passwords. Secret references (${secrets:...}) are resolved by the runtime before resource provisioning.
@PgSql)Type-safe persistence with compile-time SQL validation. Define an interface, annotate with @PgSql, and the annotation processor validates queries against your schema at build time.
@PgSql
public interface OrderPersistence {
Promise<Option<OrderRow>> findById(long id);
Promise<List<OrderRow>> findByStatus(String status);
Promise<OrderRow> save(OrderRow order);
}
The processor generates implementations that use the native async PostgreSQL driver with built-in pipelining — multiple queries in flight per connection without round-trip serialization. No external connection pooler needed.
@Sql)@Slice
public interface UserRepository {
Promise<Option<User>> findUser(FindRequest request);
static UserRepository userRepository(@Sql SqlConnector db) {
return request -> db.queryOptional(
"SELECT id, name, email FROM users WHERE id = ?",
USER_MAPPER,
request.userId());
}
}
Database migrations are bundled in the blueprint artifact and executed atomically with deployment. The cluster coordinates migration execution via consensus — no external migration tool, no separate migration step.
Migration types: versioned (V), repeatable (R), undo (U), and baseline (B). Failure recovery with automatic retry and exponential backoff.
// Custom qualifier for a specific HTTP endpoint
@ResourceQualifier(type = HttpClient.class, config = "http.payment")
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@interface PaymentHttp {}
@Slice
public interface PaymentGateway {
Promise<PaymentResult> charge(ChargeRequest request);
static PaymentGateway paymentGateway(@PaymentHttp HttpClient http) {
return request -> http.post("/charges", JsonMapper.serialize(request))
.map(result -> result.body())
.map(PaymentResult::fromJson);
}
}
The built-in @Http qualifier maps to the "http" config section. For multiple HTTP clients, create custom qualifiers as shown above. Configuration in aether.toml:
[http.payment]
base_url = "https://api.payments.example.com/v1"
connect_timeout = "5s"
request_timeout = "15s"
HttpClient methods accept string bodies and return Promise<HttpResult<String>>: get(path), post(path, body), put(path, body), delete(path), patch(path, body). All methods accept an optional Map<String, String> headers parameter.
Pub-Sub uses the same @ResourceQualifier pattern as other resources. Define custom annotations for each topic — one for publishing (targets PARAMETER) and one for subscribing (targets METHOD):
// Topic qualifiers
@ResourceQualifier(type = Publisher.class, config = "messaging.order-events")
@Retention(RUNTIME) @Target(PARAMETER)
public @interface OrderEvents {}
@ResourceQualifier(type = Subscriber.class, config = "messaging.order-events")
@Retention(RUNTIME) @Target(METHOD)
public @interface OnOrderEvent {}
// Publisher slice
@Slice
public interface OrderService {
Promise<OrderResult> placeOrder(PlaceOrderRequest request);
static OrderService orderService(@OrderEvents Publisher<OrderPlacedEvent> events,
@PgSql OrderPersistence persistence) {
return request -> persistence.save(request)
.onSuccess(order -> events.publish(
new OrderPlacedEvent(order.id())));
}
}
// Subscriber slice
@Slice
public interface InventoryUpdater {
@OnOrderEvent
Promise<Unit> onOrderPlaced(OrderPlacedEvent event);
static InventoryUpdater inventoryUpdater(@Sql SqlConnector db) {
return event -> db.update(
"UPDATE inventory SET reserved = reserved + ? WHERE item_id = ?",
event.quantity(), event.itemId())
.map(_ -> Unit.unit());
}
}
Topic subscription is managed via the consensus KV-Store. Competing consumers use round-robin distribution. Message delivery survives leader failover — proven through integration tests with node kills.
Scheduled tasks use custom @ResourceQualifier annotations targeting METHOD. Schedule parameters are configured in TOML, not in annotations:
// Qualifier annotations
@ResourceQualifier(type = Scheduled.class, config = "scheduling.daily-report")
@Retention(RUNTIME) @Target(METHOD)
public @interface DailyReport {}
@ResourceQualifier(type = Scheduled.class, config = "scheduling.alert-check")
@Retention(RUNTIME) @Target(METHOD)
public @interface AlertCheck {}
// Slice
@Slice
public interface ReportService {
@DailyReport
Promise<Unit> generateReport();
@AlertCheck
Promise<Unit> checkAlerts();
}
Schedule configuration in TOML:
[scheduling.daily-report]
cron = "0 6 * * *"
mode = "SINGLE"
[scheduling.alert-check]
interval = "5m"
mode = "SINGLE"
Two execution modes: SINGLE (one node executes, leader-selected) and ALL (every node with the slice runs independently). Cron expressions support 5-field syntax. Pause, resume, and manual trigger via CLI and REST API.
Slices can declare typed configuration dependencies. The annotation processor generates type-safe parsers at compile time.
// Define config record
public record GatewayConfig(String provider, String apiUrl, int timeoutMs) {
public static Result<GatewayConfig> gatewayConfig(String provider,
String apiUrl,
int timeoutMs) {
return Result.success(new GatewayConfig(provider, apiUrl, timeoutMs));
}
}
// Create qualifier
@ResourceQualifier(type = ConfigurationSection.class, config = "payment.gateway")
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD})
public @interface PaymentGateway {}
// Use in slice
@Slice
public interface PaymentService {
static PaymentService paymentService(
@PaymentGateway GatewayConfig config,
@PgSql PaymentPersistence persistence) { ... }
// Optional: receive config updates at runtime
@PaymentGateway
Result<Unit> onConfigUpdate(GatewayConfig newConfig);
}
Configuration merges from three sources:
| Priority | Source | When |
|---|---|---|
| 1 (highest) | KV-Store | Pushed via API, survives restarts |
| 2 | aether.toml [app.*] | Set per deployment |
| 3 (lowest) | META-INF/config.toml | Developer defaults in slice JAR |
Supported types: String, int, long, boolean, double, Option<String>, List<String>, core value objects (TimeSpan, Email, Url, Uuid), and any user-defined type with a JBCT-standard factory method.
Method-level cross-cutting concerns — retry, circuit breaker, rate limiting, logging, metrics — are declared in TOML configuration, not in code:
[interceptors.retry]
max_attempts = 3
backoff = "exponential"
initial_delay = "100ms"
[interceptors.circuit-breaker]
failure_threshold = 5
reset_timeout = "30s"
[interceptors.rate-limit]
permits_per_second = 100
Runtime enable/disable per method via CLI and REST API. No code changes, no redeployment.