Skip to content

Coming from Spring MVC

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 MVCArc
@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 bodyThe command’s own constructor properties — the command is the request body
@RequestParam / @PathVariableUnannotated query method parameters
@Autowired field or constructor injection into the controllerUnannotated handle/query parameters resolved from Spring by type, or @FromServices on a query parameter
@Valid @RequestBody X body, BindingResult resultAutomatic 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 springdocArc generates OpenAPI 3.1 from the same metadata — see Publish an OpenAPI document
A hand-written or generated fetch/axios TypeScript clientA generated, strict-mode TypeScript client — see Generate TypeScript proxies
SseEmitter or a WebSocketHandler for live dataA query returning Flow<T> or Flow.Publisher<T> — Arc hosts HTTP snapshots, SSE, and WebSocket for it automatically — see Declare observable queries
// Spring MVC
@RestController
class 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
@AllowAnonymous
data 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.

// Spring MVC
@RestController
class TaskQueryController(private val repository: TaskRepository) {
@GetMapping("/api/tasks")
fun all(): List<TaskView> = repository.all()
}
// Arc
@ReadModel
@AllowAnonymous
data 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.

  • Spring Security, @ConfigurationProperties, @Scheduled, @Transactional outside a command handler, and every other Spring Boot mechanism keep working exactly as they do today.
  • You can still write ordinary @RestController endpoints 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.