Energy-Efficient Schema-Driven Development with Spring WebMVC/WebFlux
Introduction There is a concept known as “Schema‑Driven Development.” This is a methodology in which API documentation is first defined as an OpenAPI schema, and from that schema, server and client code are automatically generated. This approach prevents discrepancies between documentation and implementation, while accelerating the development cycle and ensuring quality. However, writing OpenAPI schemas manually can be a poor developer experience. While tools like typespec exist to address this, they will not be covered here. In Spring WebMVC/WebFlux, you can use springdoc-openapi to automatically generate an OpenAPI schema from Spring controller implementations. Having an OpenAPI schema enables automatic generation of HTML documentation and client code, which already provides some value. That said, since the schema is generated from the controller implementation in this case, it may not fully qualify as schema-driven development. Did you know that since Spring 5.1, it’s possible to define controllers using interfaces? By leveraging these interfaces, you can more easily achieve schema-driven development with less overhead. Separating Traditional Controller Definitions into Interfaces Here’s an example. It uses annotations like *Exchange, which might be unfamiliar to some, but the reason for using them will be explained later. Using the standard *Mapping annotations is also perfectly fine. @HttpExchange("/api/v1/blogs") interface BlogApi { @GetExchange("/") @Operation( summary = "Get blog list", description = "blah blah blah" ) fun list(): List @PostExchange("/") @Operation( summary = "Create a new blog", description = "blah blah blah" ) fun create(@RequestBody request: BlogCreateRequest): Blog @GetExchange("/{id}") @Operation( summary = "Get a blog by ID", description = "blah blah blah" ) fun get(@PathVariable id: Int): Blog @PutExchange("/{id}") @Operation( summary = "Update a blog by ID", description = "blah blah blah" ) fun update(@PathVariable id: Int, @RequestBody request: BlogUpdateRequest): Blog @DeleteExchange("/{id}") @Operation( summary = "Delete a blog by ID", description = "blah blah blah" ) fun delete(@PathVariable id: Int) } data class Blog(val id: Int, val title: String, val body: String) data class BlogCreateRequest(val title: String, val body: String) data class BlogUpdateRequest(val title: String, val body: String) Next, you implement it like this. @RestController class BlogApiController : BlogApi { // Omitted... } Using interfaces brings the following benefits: The interface becomes the API schema This can be used to generate an OpenAPI schema and HTML documentation with springdoc-openapi Interfaces can be prepared and reviewed ahead of implementation, enabling true schema-driven development Traditionally, Swagger annotations and implementation code are mixed within controllers, making it hard to understand the overall structure In the example above, there are only a few Swagger annotations, but in real-world usage, these tend to accumulate, resulting in cluttered controllers It also makes reviewing easier for reviewers It’s also great that this approach doesn’t require any new tools, so it's easy to start. And if you decide to stop using it later, it's easy to revert. (springdoc-openapi is needed, but it’s not too difficult to integrate...) The typespec tool briefly mentioned earlier allows defining OpenAPI schemas in TypeScript-like code. Using interfaces brings a development experience similar to that. (While typespec can also be used to define JSON Schema and Protobuf, making it more powerful...) Generating OpenAPI Schemas with springdoc-openapi There’s no change in how this is done compared to traditional controller-based approaches, so this section will be omitted. (There are also plenty of articles online explaining this in detail...) Development Flow In short, you can organize your development flow like this: Advanced Usage: Using with Spring HTTP Interface Client Since Spring 6, a feature called the HTTP Interface Client has been introduced. At first, I thought it was just a knockoff of Retrofit (sorry...), but it turns out you can also use it as a controller interface. The *Exchange annotations in the earlier example come from this HTTP Interface. In other words, if the client side is also using Spring, you can extract the controller interface and related model classes into a library, and use it to easily build the API client. val webClient = WebClient.builder() .baseUrl("http://localhost:8080/") .build() val proxyFactory = HttpServiceProxyFactory.builderFor(WebClientAdapter.create(webClient)).build() val blogApi = proxyFactory.createClient(BlogApi::class.java) This is particularly useful in

Introduction
There is a concept known as “Schema‑Driven Development.” This is a methodology in which API documentation is first defined as an OpenAPI schema, and from that schema, server and client code are automatically generated. This approach prevents discrepancies between documentation and implementation, while accelerating the development cycle and ensuring quality.
However, writing OpenAPI schemas manually can be a poor developer experience. While tools like typespec exist to address this, they will not be covered here.
In Spring WebMVC/WebFlux, you can use springdoc-openapi to automatically generate an OpenAPI schema from Spring controller implementations. Having an OpenAPI schema enables automatic generation of HTML documentation and client code, which already provides some value.
That said, since the schema is generated from the controller implementation in this case, it may not fully qualify as schema-driven development.
Did you know that since Spring 5.1, it’s possible to define controllers using interfaces? By leveraging these interfaces, you can more easily achieve schema-driven development with less overhead.
Separating Traditional Controller Definitions into Interfaces
Here’s an example. It uses annotations like *Exchange
, which might be unfamiliar to some, but the reason for using them will be explained later. Using the standard *Mapping
annotations is also perfectly fine.
@HttpExchange("/api/v1/blogs")
interface BlogApi {
@GetExchange("/")
@Operation(
summary = "Get blog list",
description = "blah blah blah"
)
fun list(): List<Blog>
@PostExchange("/")
@Operation(
summary = "Create a new blog",
description = "blah blah blah"
)
fun create(@RequestBody request: BlogCreateRequest): Blog
@GetExchange("/{id}")
@Operation(
summary = "Get a blog by ID",
description = "blah blah blah"
)
fun get(@PathVariable id: Int): Blog
@PutExchange("/{id}")
@Operation(
summary = "Update a blog by ID",
description = "blah blah blah"
)
fun update(@PathVariable id: Int, @RequestBody request: BlogUpdateRequest): Blog
@DeleteExchange("/{id}")
@Operation(
summary = "Delete a blog by ID",
description = "blah blah blah"
)
fun delete(@PathVariable id: Int)
}
data class Blog(val id: Int, val title: String, val body: String)
data class BlogCreateRequest(val title: String, val body: String)
data class BlogUpdateRequest(val title: String, val body: String)
Next, you implement it like this.
@RestController
class BlogApiController : BlogApi {
// Omitted...
}
Using interfaces brings the following benefits:
- The interface becomes the API schema
- This can be used to generate an OpenAPI schema and HTML documentation with springdoc-openapi
- Interfaces can be prepared and reviewed ahead of implementation, enabling true schema-driven development
- Traditionally, Swagger annotations and implementation code are mixed within controllers, making it hard to understand the overall structure
- In the example above, there are only a few Swagger annotations, but in real-world usage, these tend to accumulate, resulting in cluttered controllers
- It also makes reviewing easier for reviewers
It’s also great that this approach doesn’t require any new tools, so it's easy to start. And if you decide to stop using it later, it's easy to revert.
(springdoc-openapi is needed, but it’s not too difficult to integrate...)
The typespec tool briefly mentioned earlier allows defining OpenAPI schemas in TypeScript-like code. Using interfaces brings a development experience similar to that. (While typespec can also be used to define JSON Schema and Protobuf, making it more powerful...)
Generating OpenAPI Schemas with springdoc-openapi
There’s no change in how this is done compared to traditional controller-based approaches, so this section will be omitted.
(There are also plenty of articles online explaining this in detail...)
Development Flow
In short, you can organize your development flow like this:
Advanced Usage: Using with Spring HTTP Interface Client
Since Spring 6, a feature called the HTTP Interface Client has been introduced.
At first, I thought it was just a knockoff of Retrofit (sorry...), but it turns out you can also use it as a controller interface. The *Exchange
annotations in the earlier example come from this HTTP Interface.
In other words, if the client side is also using Spring, you can extract the controller interface and related model classes into a library, and use it to easily build the API client.
val webClient = WebClient.builder()
.baseUrl("http://localhost:8080/")
.build()
val proxyFactory = HttpServiceProxyFactory.builderFor(WebClientAdapter.create(webClient)).build()
val blogApi = proxyFactory.createClient(BlogApi::class.java)
This is particularly useful in environments where Spring is widely used in microservices.
However, since the interface is shared between server and client, it’s a downside that only one side cannot be Reactive(Mono/Flux).
Conclusion
This article introduced the use of controller interfaces as one way to implement schema-driven development in Spring WebMVC/WebFlux.
Traditionally, OpenAPI schemas were generated from Spring controller implementations, which did not align well with the schema-driven development philosophy. By leveraging controller interfaces available since Spring 5.1, it’s now possible to define the schema first and then base both implementations and client generation on it.
Additionally, these interfaces can be used in combination with the HTTP Interface Client introduced in Spring 6.
Some might argue that if you want true schema-driven development, you should use gRPC. However, the approach introduced here offers a development experience that gets you fairly close.