Getting Started Developer Guide Feature Catalog Operator Guide Reference
Docs/Developer Guide

Aether Developer Guide

Build distributed services with typed interfaces and declarative resources.

Slices Are Services

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 injectionFactory method parameters
Service method callPromise<T> method call (local or remote, transparent)
@RestControllerroutes.toml (declarative HTTP routing)
Kubernetes Deployment YAMLBlueprint 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);
    }
}

Resource Provisioning Model

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 PatternClassificationWhat Happens
@Sql SqlConnector dbResource dependencyProvisioned from aether.toml
InventoryService inventorySlice dependencyProxy generated for remote calls
OrderValidator validatorPlain interfaceFactory method called directly

This separation has immediate consequences:

Built-In Qualifiers

Aether ships four built-in resource qualifiers:

AnnotationResource TypeConfig Section
@PgSqlPgSqlConnector"database"
@SqlSqlConnector"database"
@HttpHttpClient"http"
@NotifyNotificationSender"notification"

Custom Qualifiers

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.

Database Access

Aether Store (@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.

Generic SQL (@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());
    }
}

Schema Migrations

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.

HTTP Clients

// 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 Messaging

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

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.

Application Configuration

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:

PrioritySourceWhen
1 (highest)KV-StorePushed via API, survives restarts
2aether.toml [app.*]Set per deployment
3 (lowest)META-INF/config.tomlDeveloper 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.

Interceptors

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.

Next Steps