Bosca / Developers

Building a feature

From a model, to a live API

Add a feature the way the platform does. A model, a repository, a service, and a controller — each a few annotated lines, each wired into the running server. Here is the whole slice.

1 · Model

Start with a model

A plain Kotlin data class in the core module — the one shape your feature is about. It's just the data; the repository, service, and API you build next give it behavior.

  • A plain data class in the core module.
  • Shared by the repository, service, and API you build next.
  • Just the data — behavior comes from the layers around it.
Task.kt
data class Task(
  val id: UUID,
  val title: String,
  val status: TaskStatus,
)

2 · Repository

Query with @Repository

Annotate a class @Repository and write your queries as raw SQL with named parameters. The processor generates the JDBC — the statement, the parameter binding, the row mapping to your model — as a suspending function. You write the SQL; the rest is generated.

  • Raw SQL with :named parameters, mapped to your model.
  • Every call is a suspending function.
  • Column names bind explicitly — no hidden conventions.
TaskRepository.kt
@Repository
class TaskRepository {

  @Query("SELECT * FROM tasks WHERE status = :status")
  suspend fun byStatus(status: TaskStatus): List<Task>
}

3 · Service

Logic in a service

@ServiceImplementation registers your service with the container, its dependencies injected through the constructor. The service is the boundary every caller goes through — controllers and other subsystems talk to it, never to the repository directly.

  • Constructor injection, resolved at startup.
  • The one place your feature's rules live.
  • Repositories stay behind the service, always.
TaskServiceImpl.kt
@ServiceImplementation
class TaskServiceImpl(
  private val tasks: TaskRepository,
) : TaskService {

  override suspend fun open() =
    tasks.byStatus(TaskStatus.OPEN)
}

4 · API

Expose it — GraphQL or REST

Declare the type in the schema, then bind a controller with @TypeController and @Field — your fields join the one schema every subsystem shares. Prefer REST? Annotate a handler @RouteController with its path, method, and authentication. Either way, the endpoint is live in the same server.

  • Schema-first GraphQL: your type sits in the shared graph.
  • Or a REST route, with auth declared right on the handler.
  • Push live updates to clients with a subscription.
TaskController.kt
@TypeController("Query")
class TaskController(
  private val tasks: TaskService,
) {

  @Field
  suspend fun openTasks() = tasks.open()
}

Keep exploring

More for developers