---
title: Coming from Spring MVC
description: Map familiar @RestController, validation, and dependency-injection conventions onto Arc's model-bound commands and queries.
---


## 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](/arc/backend/kotlin/guides/commands/#add-validation) |
| `ResponseEntity<T>` | A `CommandResult<T>` or `QueryResult<T>` JSON envelope — see the [HTTP contract reference](/arc/backend/kotlin/reference/http-contract/) |
| `@PreAuthorize("hasRole('ADMIN')")` | `@Authorize(roles = ["admin"])` or `@Roles("admin")` on the class or the operation — see [Protect the command](/arc/backend/kotlin/guides/commands/#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](/arc/backend/kotlin/guides/openapi/) |
| A hand-written or generated `fetch`/axios TypeScript client | A generated, strict-mode TypeScript client — see [Generate TypeScript proxies](/arc/backend/kotlin/guides/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](/arc/backend/kotlin/guides/observable-queries/) |

## A command, side by side

```kotlin
// 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))
    }
}
```

```kotlin
// 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`.

## A query, side by side

```kotlin
// Spring MVC
@RestController
class TaskQueryController(private val repository: TaskRepository) {
    @GetMapping("/api/tasks")
    fun all(): List<TaskView> = repository.all()
}
```

```kotlin
// 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](/arc/backend/kotlin/guides/queries/#add-a-query-to-a-read-model)
for a query that takes both.

## What does not change

- 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](/arc/backend/kotlin/guides/spring-data/) for the paging, sorting, and read-model
  resolution Arc adds on top of them.

## Where to go next

- [Build your first Arc application in Kotlin](/arc/backend/kotlin/get-started/) or
  [in Java](/arc/backend/kotlin/get-started/java/).
- [Create and validate commands](/arc/backend/kotlin/guides/commands/) and
  [Expose one-shot and observable queries](/arc/backend/kotlin/guides/queries/) for the complete contract.
- [Why Arc for Kotlin and Java](/arc/backend/kotlin/why-arc-kotlin/) for the problem this all removes.
