---
title: "Kotlin com PostgreSQL: Backend Prático em 2026 | Kotlin Brasil"
url: "https://kotlin.dev.br/tutoriais/kotlin-postgresql-backend/"
markdown_url: "https://kotlin.dev.br/tutoriais/kotlin-postgresql-backend.MD"
description: "Aprenda a usar Kotlin com PostgreSQL no backend: schema, conexão, Exposed, migrations, transações, testes e boas práticas para APIs em produção."
date: "2026-05-20"
author: "Karina Melo"
---

# Kotlin com PostgreSQL: Backend Prático em 2026 | Kotlin Brasil

Aprenda a usar Kotlin com PostgreSQL no backend: schema, conexão, Exposed, migrations, transações, testes e boas práticas para APIs em produção.


Usar **Kotlin com PostgreSQL** é uma das combinações mais fortes para quem quer trabalhar com backend JVM em 2026. Kotlin entrega uma linguagem concisa, segura contra nulos e muito produtiva; PostgreSQL entrega um banco relacional maduro, confiável, com ótimo suporte a índices, JSONB, transações e extensões. Juntos, eles aparecem com frequência em APIs internas, fintechs, e-commerce, plataformas SaaS e vagas de backend Kotlin no Brasil.

Este tutorial mostra um caminho prático para sair do "consigo conectar no banco" e chegar em uma estrutura que dá para evoluir: schema organizado, conexão com pool, acesso via Exposed, migrations, transações, testes e decisões de produção. Se você está estudando backend, leia também o [roadmap para desenvolvedor backend Kotlin](/carreira/roadmap-dev-backend-kotlin/), o [guia de Ktor](/guias/guia-kotlin-backend-ktor/) e o [tutorial de Spring Boot com Kotlin](/tutoriais/kotlin-spring-boot-tutorial/).

## Quando escolher PostgreSQL em um backend Kotlin?

PostgreSQL é uma boa escolha quando seus dados têm relacionamento claro, precisam de consistência e serão consultados por filtros variados. Usuários, pedidos, pagamentos, permissões, auditoria, assinaturas e catálogos são exemplos clássicos. Mesmo quando o produto cresce, o PostgreSQL continua competitivo porque oferece transações ACID, constraints, índices parciais, views, funções, JSONB e extensões como `pg_trgm` para busca textual.

Em Kotlin, ele combina bem com três estilos comuns:

- **Ktor + Exposed** para APIs leves e idiomáticas;
- **Spring Boot + Spring Data JPA** para sistemas corporativos com ecossistema amplo;
- **jOOQ** quando o time quer SQL explícito e geração de código type-safe.

Não existe uma única resposta certa. Para aprender e criar APIs enxutas, Ktor com Exposed é direto. Para times que já usam Spring, Spring Boot reduz atrito. Para domínios com SQL complexo, jOOQ pode ser mais transparente que um ORM tradicional.

## Dependências básicas com Gradle Kotlin DSL

O exemplo abaixo usa Exposed com JDBC e HikariCP. É um ponto de partida simples para uma API Ktor ou um serviço Kotlin puro:

```kotlin
plugins {
    kotlin("jvm") version "2.3.20"
    application
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.jetbrains.exposed:exposed-core:1.0.0")
    implementation("org.jetbrains.exposed:exposed-dao:1.0.0")
    implementation("org.jetbrains.exposed:exposed-jdbc:1.0.0")
    implementation("org.jetbrains.exposed:exposed-java-time:1.0.0")

    implementation("org.postgresql:postgresql:42.7.4")
    implementation("com.zaxxer:HikariCP:6.2.1")

    testImplementation(kotlin("test"))
}
```

Se você está usando Ktor, combine isso com `ContentNegotiation`, `kotlinx.serialization` e rotas HTTP. Se está usando Spring Boot, talvez prefira `spring-boot-starter-data-jpa` ou `spring-jdbc`, mas os conceitos de schema, migrations, transações e testes continuam os mesmos.

## Configurando o pool de conexões

Nunca abra uma nova conexão manualmente a cada request. Em produção, use um pool. O HikariCP é o padrão de fato no ecossistema JVM e funciona bem com Kotlin:

```kotlin
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import org.jetbrains.exposed.sql.Database

fun conectarPostgres() {
    val config = HikariConfig().apply {
        jdbcUrl = System.getenv("DATABASE_URL")
            ?: "jdbc:postgresql://localhost:5432/app"
        username = System.getenv("DATABASE_USER") ?: "app"
        password = System.getenv("DATABASE_PASSWORD") ?: "app"
        maximumPoolSize = 10
        minimumIdle = 2
        isAutoCommit = false
        transactionIsolation = "TRANSACTION_REPEATABLE_READ"
        validate()
    }

    Database.connect(HikariDataSource(config))
}
```

O detalhe importante é tratar configuração como ambiente, não como código. `DATABASE_URL`, usuário e senha devem vir do ambiente de deploy ou do gerenciador de secrets. Em desenvolvimento local, um `docker-compose.yml` com PostgreSQL resolve. Em produção, prefira um serviço gerenciado quando o time não quer operar backup, replicação e atualizações manualmente.

## Modelando tabelas com Exposed

Exposed representa tabelas como objetos Kotlin. Isso dá autocompletar, tipos explícitos e menos strings soltas espalhadas pelo código:

```kotlin
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.javatime.datetime

object Clientes : Table("clientes") {
    val id = uuid("id")
    val nome = varchar("nome", 160)
    val email = varchar("email", 255).uniqueIndex()
    val ativo = bool("ativo").default(true)
    val criadoEm = datetime("criado_em")

    override val primaryKey = PrimaryKey(id)
}

object Pedidos : Table("pedidos") {
    val id = uuid("id")
    val clienteId = uuid("cliente_id").references(Clientes.id)
    val status = varchar("status", 40)
    val totalCentavos = long("total_centavos")
    val criadoEm = datetime("criado_em")

    override val primaryKey = PrimaryKey(id)
}
```

Mesmo usando DSL, pense primeiro no banco. Defina chaves primárias, chaves estrangeiras, constraints, índices e nomes estáveis. Kotlin ajuda na camada de aplicação, mas a integridade dos dados deve continuar protegida no PostgreSQL. Um bug em uma rota não pode ser capaz de gravar pedido sem cliente ou e-mail duplicado se o banco estiver modelado corretamente.

## CRUD simples com transações

Toda operação que lê e escreve dados relacionados deve estar dentro de uma transação. Com Exposed, o bloco `transaction` deixa isso explícito:

```kotlin
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import java.time.LocalDateTime
import java.util.UUID

data class NovoCliente(val nome: String, val email: String)

fun criarCliente(input: NovoCliente): UUID = transaction {
    val id = UUID.randomUUID()

    Clientes.insert {
        it[Clientes.id] = id
        it[nome] = input.nome.trim()
        it[email] = input.email.lowercase().trim()
        it[ativo] = true
        it[criadoEm] = LocalDateTime.now()
    }

    id
}

fun listarClientesAtivos(): List<NovoCliente> = transaction {
    Clientes
        .selectAll()
        .where { Clientes.ativo eq true }
        .map { row ->
            NovoCliente(
                nome = row[Clientes.nome],
                email = row[Clientes.email]
            )
        }
}
```

Para APIs Ktor, chame funções de repositório a partir das rotas e mantenha validação de entrada antes da transação. Para Spring Boot, encapsule regras em services e use `@Transactional` quando fizer sentido. Em qualquer stack, evite misturar regra de negócio complexa diretamente no handler HTTP.

## Migrations: não crie schema no startup

É tentador usar `SchemaUtils.create()` em todo boot. Isso serve para protótipos, mas não para produção. Em um sistema real, mudanças no banco precisam ser versionadas. Use Flyway ou Liquibase para manter um histórico claro:

```sql
-- V1__criar_clientes_e_pedidos.sql
CREATE TABLE clientes (
    id UUID PRIMARY KEY,
    nome VARCHAR(160) NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE,
    ativo BOOLEAN NOT NULL DEFAULT true,
    criado_em TIMESTAMP NOT NULL
);

CREATE TABLE pedidos (
    id UUID PRIMARY KEY,
    cliente_id UUID NOT NULL REFERENCES clientes(id),
    status VARCHAR(40) NOT NULL,
    total_centavos BIGINT NOT NULL,
    criado_em TIMESTAMP NOT NULL
);

CREATE INDEX idx_pedidos_cliente_id ON pedidos(cliente_id);
CREATE INDEX idx_pedidos_status ON pedidos(status);
```

Migrations são especialmente importantes quando há mais de uma instância da aplicação rodando, quando o deploy é automatizado ou quando você precisa auditar por que uma coluna mudou. Elas também ajudam entrevistas e portfólio: demonstram que você entende ciclo de vida de banco, não apenas código Kotlin.

## Índices e consultas que importam

Um erro comum é criar índice em toda coluna. Índice acelera leitura, mas custa escrita e espaço. Comece pelos filtros reais da aplicação:

- `email` único para login ou identificação;
- chaves estrangeiras usadas em joins;
- `status` quando filas operacionais filtram pedidos pendentes;
- `criado_em` quando telas listam eventos recentes;
- índices compostos quando a consulta filtra por mais de uma coluna.

Use `EXPLAIN ANALYZE` quando uma query ficar lenta. Em Kotlin, a abstração não elimina a necessidade de entender SQL. Se uma tela lista os últimos pedidos de um cliente, um índice em `(cliente_id, criado_em DESC)` pode ser mais útil que dois índices separados.

## Testando com PostgreSQL real

Para lógica de domínio pura, testes unitários bastam. Para repositórios, teste contra PostgreSQL real sempre que possível. SQLite em memória não se comporta igual: tipos, constraints, JSONB, locks e funções diferem. Com Testcontainers, você sobe um PostgreSQL descartável no teste e valida migrations, queries e transações.

Mesmo que você não use Testcontainers no começo, pelo menos rode uma suíte local contra Docker antes de publicar mudanças importantes. Bugs de banco costumam aparecer tarde: timezone errado, coluna `nullable` sem querer, índice ausente, transação longa demais, deadlock por ordem inconsistente de atualização.

## Checklist para produção

Antes de colocar um backend Kotlin com PostgreSQL no ar, confira:

- migrations versionadas e revisadas;
- pool de conexões com tamanho compatível com o banco;
- timeouts configurados para queries e requests;
- logs sem senha, token ou dados sensíveis;
- backup e restore testados;
- índices para consultas críticas;
- transações curtas;
- validação de entrada antes de gravar;
- métricas de latência do banco e erros SQL;
- secrets fora do repositório.

Esse checklist parece operacional, mas impacta diretamente a carreira. Muitas vagas de backend Kotlin pedem Spring Boot, Ktor, PostgreSQL, mensageria, Docker e cloud no mesmo pacote. Saber conectar esses pontos torna seu perfil mais forte do que apenas decorar sintaxe da linguagem.

## Próximos passos

Se você quer evoluir este tema, construa uma API pequena com cadastro de clientes, pedidos e autenticação. Comece com [Ktor e APIs REST](/tutoriais/kotlin-ktor-tutorial/), adicione PostgreSQL com Exposed, documente o contrato com [OpenAPI e Swagger](/tutoriais/kotlin-ktor-openapi-swagger/) e depois escreva testes de integração. Para comparar caminhos, leia também [Exposed 1.0 com R2DBC](/blog/exposed-1-0-r2dbc-kotlin-2026/) e o guia de [microsserviços com Kotlin](/guias/guia-kotlin-microservicos/).

Kotlin com PostgreSQL não é uma combinação exótica: é uma base sólida para backend profissional. O diferencial está em usar a linguagem para escrever código mais claro sem abrir mão dos fundamentos de banco relacional: schema bem desenhado, transações corretas, SQL entendido e deploy seguro.
