Coming from Spring MVC
The mapping
Section titled “The mapping”Arc replaces the controller layer, not Spring itself. Dependency injection, @Configuration,
@Bean, and every other ordinary Spring Boot mechanism still work exactly as before — the
difference is what you write to expose a command or a query over HTTP.
| Spring MVC | Arc |
|---|---|
@RestController class with a @PostMapping method | @Command class with a public handle method |
@RestController class with a @GetMapping method | @ReadModel class with a static or @JvmStatic companion query method |
@RequestBody CreateTaskRequest body | The command’s own constructor properties — the command is the request body |
@RequestParam / @PathVariable | Unannotated query method parameters |
@Autowired field or constructor injection into the controller | Unannotated handle/query parameters resolved from Spring by type, or @FromServices on a query parameter |
@Valid @RequestBody X body, BindingResult result | Automatic Jakarta validation on the command or query-argument graph when a Validator bean exists — see Add validation |
ResponseEntity<T> | A CommandResult<T> or QueryResult<T> JSON envelope — see the HTTP contract reference |
@PreAuthorize("hasRole('ADMIN')") | @Authorize(roles = ["admin"]) or @Roles("admin") on the class or the operation — see Protect the command |
| A hand-written OpenAPI annotation set, or springdoc | Arc generates OpenAPI 3.1 from the same metadata — see Publish an OpenAPI document |
A hand-written or generated fetch/axios TypeScript client | A generated, strict-mode TypeScript client — see Generate TypeScript proxies |
SseEmitter or a WebSocketHandler for live data | A query returning Flow<T> or Flow.Publisher<T> — Arc hosts HTTP snapshots, SSE, and WebSocket for it automatically — see Declare observable queries |
A command, side by side
Section titled “A command, side by side”// Spring MVC@RestControllerclass TaskController(private val repository: TaskRepository) { @PostMapping("/api/create-task") fun create(@Valid @RequestBody request: CreateTaskRequest): ResponseEntity<TaskCreated> { val task = repository.create(request.title) return ResponseEntity.ok(TaskCreated(task.id, task.title)) }}// Arc@Command@AllowAnonymousdata class CreateTask(val title: String) { fun handle(repository: TaskRepository): TaskCreated { val task = repository.create(title) return TaskCreated(task.id, task.title) }}There is no TaskController and no separate CreateTaskRequest DTO. CreateTask is the request
body, repository is resolved from Spring the same way constructor injection would resolve it, and
the route (POST /api/create-task by convention) is derived and generated by KSP rather than
declared with @PostMapping.
A query, side by side
Section titled “A query, side by side”// Spring MVC@RestControllerclass TaskQueryController(private val repository: TaskRepository) { @GetMapping("/api/tasks") fun all(): List<TaskView> = repository.all()}// Arc@ReadModel@AllowAnonymousdata class TaskView(val id: String, val title: String) { companion object { @JvmStatic @Path("/api/tasks") fun all(@FromServices repository: TaskRepository): List<TaskView> = repository.all() }}@FromServices exists precisely to distinguish repository — a dependency — from an unannotated
parameter, which a caller supplies as a query argument. Without a query taking caller arguments,
every parameter here happens to be a dependency; see Add a query to a read model
for a query that takes both.
What does not change
Section titled “What does not change”- Spring Security,
@ConfigurationProperties,@Scheduled,@Transactionaloutside a command handler, and every other Spring Boot mechanism keep working exactly as they do today. - You can still write ordinary
@RestControllerendpoints alongside Arc’s generated ones in the same application — Arc adds routes, it does not remove Spring MVC. - Spring Data JPA and MongoDB repositories are injected the same way; see Use Spring Data read models for the paging, sorting, and read-model resolution Arc adds on top of them.
Where to go next
Section titled “Where to go next”- Build your first Arc application in Kotlin or in Java.
- Create and validate commands and Expose one-shot and observable queries for the complete contract.
- Why Arc for Kotlin and Java for the problem this all removes.