skip to content

Contrast the functional model with the legacy @EnableBinding/@StreamListener model. How do you migrate, and why was the annotation model deprecated?

level: seniorimportance: should knowfreq 50%

answer

  1. @EnableBinding + @StreamListener -> functional beans
  2. Sink=Consumer, Processor=Function, Source=Supplier
  3. input/output channels -> -in-0/-out-0
  4. removed in Spring Cloud Stream 4.0
  5. output().send -> StreamBridge

basics

~20 s

Old code used @EnableBinding with channel interfaces (Source/Sink/Processor) and @StreamListener methods bound to @Input/@Output channels. The new model replaces all of that with plain Supplier/Function/Consumer beans named in spring.cloud.function.definition. Migrate by turning each listener into a function.

solid answer

~40 s

The legacy model declared binding interfaces (Sink, Source, Processor or custom @Input/@Output channel interfaces), enabled them with @EnableBinding, and attached handlers via @StreamListener referencing a channel name. It was verbose, coupled business logic to SCSt annotations, and hard to unit-test. The functional model removes all annotations: a Consumer becomes a sink, a Function a processor, a Supplier a source, activated via spring.cloud.function.definition. Migration steps: replace each @StreamListener method with a functional bean whose logic is the method body; drop @EnableBinding and the channel interfaces; rename config keys from `input`/`output` to `<fn>-in-0`/`<fn>-out-0` (or alias them). It was deprecated and then removed because Spring Cloud Function gave a cleaner, portable, composable, framework-agnostic abstraction, and the annotation machinery duplicated that.

code

java · 21 lines
java
// BEFORE (legacy, removed in 4.0)
@EnableBinding(Processor.class)
public class LegacyUpper {
    @StreamListener(Processor.INPUT)
    @SendTo(Processor.OUTPUT)
    public String handle(String in) {
        return in.toUpperCase();
    }
}

// AFTER (functional)
@Configuration
public class FunctionalUpper {
    @Bean
    public Function<String, String> uppercase() {
        return String::toUpperCase;
    }
}
// spring.cloud.function.definition=uppercase
// spring.cloud.stream.bindings.uppercase-in-0.destination=words
// spring.cloud.stream.bindings.uppercase-out-0.destination=upper-words

go deeper

for a junior

Know that @EnableBinding/@StreamListener is the old way and functional beans are the new way.

for a middle

Map Sink/Processor/Source to Consumer/Function/Supplier and rename config keys.

for a senior

Walk a full migration including StreamBridge, routing, error channels, and the alias trick for legacy config keys.

for a principal

Justify the deprecation architecturally (Spring Cloud Function convergence), plan a phased migration across many services, and handle removed-API breakage at 4.0.

**The legacy annotation model** (SCSt 1.x–2.x) worked like this: - You defined a **binding interface** with channel accessors, e.g. the built-ins `Source` (one `@Output MessageChannel output()`), `Sink` (one `@Input SubscribableChannel input()`), and `Processor` (both), or your own custom interface with `@Input`/`@Output` methods. - You activated it with **`@EnableBinding(Processor.class)`**, which created the channels as beans. - You attached handlers with **`@StreamListener(Processor.INPUT)`** on a method, optionally with `@SendTo(Processor.OUTPUT)` to route the return value. Example legacy processor: ```java @EnableBinding(Processor.class) public class UpperProcessor { @StreamListener(Processor.INPUT) @SendTo(Processor.OUTPUT) public String handle(String in) { return in.toUpperCase(); } } ``` **The functional equivalent** collapses to: ```java @Bean public Function<String,String> uppercase() { return String::toUpperCase; } // spring.cloud.function.definition=uppercase ``` **Mapping table for migration:** | Legacy | Functional | |---|---| | `@StreamListener` sink (void) | `Consumer<T>` | | `@StreamListener` + `@SendTo` | `Function<T,R>` | | `@InboundChannelAdapter` / poller source | `Supplier<T>` | | `@EnableBinding(Xxx.class)` | *(deleted)* | | channel `input` | binding `<fn>-in-0` | | channel `output` | binding `<fn>-out-0` | | `@StreamListener(condition=...)` | route via routing function | **Config migration.** Old keys used the channel name: `spring.cloud.stream.bindings.input.destination=...`. New keys use the generated binding name: `spring.cloud.stream.bindings.uppercase-in-0.destination=...`. If you must keep the old destination keys (large existing config), alias the binding: `spring.cloud.stream.function.bindings.uppercase-in-0=input`, then keep `...bindings.input.destination`. **Why deprecated/removed.** Spring Cloud Function already provided a clean, typed, framework-independent way to express message handlers as `java.util.function` types with a `FunctionCatalog`, composition, and content-type conversion. The `@EnableBinding` layer duplicated that with heavier, less-testable, SCSt-specific machinery. The annotation model was deprecated in 3.1 and **removed in 4.0**. The functional style is unit-testable without a broker (just call the function), portable across web/stream/task, and supports composition and reactive types. **Gotchas during migration.** - Manual sends that used `output().send(...)` should become `StreamBridge` (imperative ad-hoc sends) or a `Supplier`. - `@StreamListener` conditional dispatch has no direct functional twin; use a routing function (`RoutingFunction`, definition `functionRouter`, with `spring.cloud.function.routing-expression`) or header routing. - Error-handling channels change name to `<destination>.<group>.errors`; verify custom error handlers still bind. - Reactive `@StreamListener` methods returning `Flux` map to `Function<Flux<..>,Flux<..>>`.

  • In the legacy model you called `processor.output().send(msg)` from a REST controller. What replaces that imperatively?
    Inject `StreamBridge` and call `streamBridge.send("someBinding-out-0", payload)`. StreamBridge is the functional-model way to do ad-hoc, imperative sends that aren't driven by an incoming message; a Supplier is the alternative for continuous/polled sources.
  • How do you replicate @StreamListener's conditional routing in the functional model?
    Use Spring Cloud Function's routing: set spring.cloud.function.definition=functionRouter (the built-in RoutingFunction) with spring.cloud.function.routing-expression, or a message header `spring.cloud.function.definition`, to dispatch each message to a named function.

saying these in an interview costs you the question

  • Claiming the annotation model still coexists with functional in 4.0
  • Trying to keep @EnableBinding channel interfaces alongside functions
  • Not knowing StreamBridge replaces manual channel.send calls

context