Skip to content
Spice Framework on GitHub

Core expressions, interception, and bean composition

frameworkMaturity: previewSource: spice@c0641b3Exact reviewed source

Spice provides the useful outcomes of Spring expressions, AOP, and hierarchical bean configuration without introducing a reflective runtime container.

Restricted expressions

expression.Compile accepts a caller-owned schema containing only explicitly declared Boolean/string variables and functions. The language supports !, &&, ||, ==, !=, literals, calls, and parentheses. Programs are typed, immutable, bounded, context-aware, and evaluated against positional bindings. They cannot traverse properties, resolve bean names, invoke arbitrary methods, assign, allocate, perform I/O, or access ambient process state.

@security.Authorize is the first generated integration:

// @Authorize(
// allScopes=["orders:read"],
// expression="authenticated && issuer == \"https://issuer.example\" && hasRole(\"reader\")",
// )

The compiler validates the expression at its annotation. Generated Go carries the exact validated literal into an immutable security.Policy; policy construction reuses the same parser and schema. The available symbols are:

Symbol Type
authenticated Boolean
subject string
issuer string
hasRole(string) Boolean
hasScope(string) Boolean

An expression policy always requires an authenticated principal. Expression requirements combine with role/scope requirements using AND semantics.

Typed interception

intercept.Invocation[Request, Response] and intercept.Interceptor[Request, Response] are ordinary Go functions. The first interceptor in intercept.Chain is outermost. Nil functions, nil contexts, cancellation, and wrong construction fail closed.

For every typed request/response route, generated contracts expose a typed field:

options := spicegen.ApplicationOptions{
Interceptors: spicegen.RouteInterceptors{
ControllerGet: []intercept.Interceptor[
orders.GetOrderRequest,
orders.OrderResponse,
]{
observeOrderRead,
},
},
}

Generated terminal invocations contain the direct controller call and any generated transaction or cache boundary. The configured chain is frozen while the application is constructed. Debuggers therefore show the interceptor, generated boundary, and handwritten method as ordinary frames. Raw HTTP and form/binding-result routes retain their explicit web.Middleware boundary.

Spice annotations for transactions, caching, authorization, observations, and HTTP middleware remain purpose-built generated decorators. Spice does not select methods through runtime pointcut strings, synthesize subclasses, or intercept concrete self-invocation.

Immutable bean override layers

Every generated application with overridable singleton beans exposes BeanOverrideLayer and ComposeBeanOverrides:

overrides, err := spicegen.ComposeBeanOverrides(
spicegen.BeanOverrideLayer{
Name: "library",
Overrides: spicegen.BeanOverrides{
Sender: bean.Replace(testSender),
},
},
spicegen.BeanOverrideLayer{
Name: "application",
Overrides: spicegen.BeanOverrides{
Sender: bean.Replace(applicationSender),
},
},
)

Layer names must be unique and canonical. Layers are applied parent-to-child; a later enabled typed field deliberately replaces the earlier field. The result is passed through ApplicationOptions.Overrides before construction. The running graph is never mutated, and construction, cleanup, rollback, module ownership, and exact Go type checks remain unchanged.

Compile-time auto-configuration supplies library defaults separately through reviewed fallback factories. Override layers are for explicit embedding and tests, not dependency-by-presence activation.

Boundary with Java mechanisms

Go has no classloader or subclass-proxy model. Spice therefore treats literal load-time weaving as not applicable. Its equivalent is deterministic build-time generation with source maps and inspectable Go. Universal runtime pointcuts and unrestricted expression evaluation remain deliberate non-goals; their useful application outcomes are the typed contracts above.