Spring Boot 3.x + Multi-Tenant + JWT + Kafka + Redis — production-ready desde o primeiro commit.
- Visão Geral
- Arquitetura
- Pré-requisitos
- Como Rodar Localmente
- Módulos
- Autenticação e RBAC
- Multi-tenancy
- Como Criar um Novo Tenant
- Como Adicionar um Novo Módulo
- Mensageria Kafka
- Cache Redis
- Observabilidade
- OpenAPI / Swagger
- Deploy
- Qualidade de Código
Este boilerplate encapsula decisões de arquitetura enterprise para aplicações Java que precisam suportar:
- Multi-tenancy com isolamento por schema PostgreSQL
- Autenticação stateless via JWT (access + refresh token)
- RBAC fino com permissões no formato
RESOURCE:ACTION - Mensageria confiável via Kafka com Outbox Pattern
- Cache distribuído com Redis
- Observabilidade com Prometheus, Actuator e logs JSON estruturados
O projeto segue Arquitetura Hexagonal (Ports & Adapters), organizado como Maven multi-module:
java-boilerplate/
├── core/ ← Domínio puro (entidades, exceções, ports)
├── application/ ← Use-cases (orquestração de domínio)
├── infrastructure/ ← Adapters de saída (JPA, Redis, Kafka, JWT)
├── web/ ← Adapter de entrada (REST controllers, filtros, DTOs)
├── bootstrap/ ← Ponto de entrada Spring Boot, configurações globais
├── docker/ ← Dockerfile + docker-compose
└── k8s/ ← Manifests Kubernetes
Bootstrap
├── Web → Core, Application
├── Infrastructure → Core, Application
└── Application → Core
└── Core (nenhuma dependência externa)
A regra é verificada automaticamente em build pelo ArchUnit — violações falham o CI.
| Ferramenta | Versão mínima |
|---|---|
| Java (JDK) | 21 |
| Maven | 3.9 |
| Docker + Docker Compose | 24.x |
| PostgreSQL | 16 (via Docker) |
| Redis | 7 (via Docker) |
| Kafka | 3.x (via Docker) |
git clone <url>
cd java-boilerplate
cp .env.example .env
# Edite .env com seus valores (JWT_SECRET deve ter ≥ 32 chars)docker compose -f docker/docker-compose.yml up -dIsso inicia PostgreSQL, Redis, Kafka e Zookeeper.
./mvnw spring-boot:run -pl bootstrap -Dspring-boot.run.profiles=localA aplicação estará disponível em http://localhost:8080.
http://localhost:8080/swagger-ui.html
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@acme.com","password":"secret","tenantId":"acme"}'| Módulo | Responsabilidade |
|---|---|
core |
Entidades de domínio User, Role, Permission, Tenant; hierarquia de exceções; interfaces de ports |
application |
Implementações de use-cases: LoginService, RefreshTokenService, LogoutService |
infrastructure |
JPA entities + repositories, TenantAwareDataSource, JWT, Redis, Kafka, Outbox |
web |
AuthController, DTOs, MdcFilter, TenantResolutionFilter, GlobalExceptionHandler |
bootstrap |
BoilerplateApplication, SecurityConfig, RedisConfig, KafkaConfig, OpenApiConfig |
POST /auth/login → { access_token (15min), refresh_token (7d) }
POST /auth/refresh → { novo access_token, refresh_token rotacionado }
POST /auth/logout → invalida refresh_token no Redis
Permissões seguem o formato RESOURCE:ACTION:
// No controller ou service:
@RequiresPermission("bet:place")
public void placeBet(PlaceBetCommand cmd) { ... }
// Equivalente a:
@PreAuthorize("hasAuthority('PERMISSION_bet:place')")ADMIN ──(herda de)──▶ OPERATOR
Ao atribuir ADMIN a um usuário, ele recebe todas as permissões de OPERATOR e as próprias de ADMIN.
Estratégia: Schema-per-tenant no PostgreSQL.
- Schema
public→ tabelatenants(catálogo global) - Schema
{schema_name}→users,roles,permissions,outbox_events
- Header
X-Tenant-ID - Claim
tenantIdno JWT - Subdomínio HTTP (e.g.,
acme.api.company.com)
// O TenantContextHolder é gerenciado automaticamente pelo TenantResolutionFilter.
// Para uso manual (ex: jobs agendados):
TenantContextHolder.set("acme");
try {
// lógica que acessa o banco do tenant "acme"
} finally {
TenantContextHolder.clear(); // SEMPRE em finally
}- Insira um registro na tabela
public.tenants:
INSERT INTO public.tenants (name, schema_name, active)
VALUES ('ACME Corp', 'acme', true);- Chame
TenantSchemaInitializerou execute programaticamente:
FlywayTenantMigrationConfig.migrateNewTenant(dataSourceProperties, "acme");- O schema
acmeé criado e as migrationsT*.sqlsão aplicadas automaticamente.
Em produção, crie um endpoint
POST /tenantsprotegido por@RequiresPermission("tenant:manage").
Passo 1 — Core: Defina a entidade Bet e a interface BetRepository
Passo 2 — Core/Port In: Crie PlaceBetUseCase
Passo 3 — Application: Implemente PlaceBetService com @Service
Passo 4 — Infrastructure: Crie BetJpaEntity, BetJpaRepository, BetRepositoryAdapter
Passo 5 — Web: Crie BetController, BetRequest, BetResponse
Passo 6 — Migrations: Adicione T{n}__create_bets_table.sql em db/tenant-migration/
As regras do ArchUnit validarão automaticamente que você não violou as dependências entre camadas.
@Service
public class PlaceBetService {
private final OutboxEventStore outboxEventStore;
@Transactional
public void placeBet(PlaceBetCommand cmd) {
// ... lógica de negócio
outboxEventStore.save(BetPlacedEvent.of(bet.getId(), tenantId, correlationId));
// Evento salvo na mesma transação — publicado no Kafka pelo OutboxRelay
}
}@Component
public class BetPlacedConsumer {
@KafkaListener(topics = "boilerplate.bet.events", groupId = "${app.kafka.consumer-group:bets}")
public void onBetPlaced(ConsumerRecord<String, String> record) {
// TenantContext já está populado pelo TenantAwareConsumerInterceptor
// ...
}
}Eventos que falham após retries vão automaticamente para {topic}.DLT (Dead Letter Topic).
@Service
public class MyService {
private final CacheService cache;
public Data getData(String id) {
String key = CacheService.key(tenantId, "my-data", id);
return cache.get(key, Data.class)
.orElseGet(() -> {
Data data = loadFromDb(id);
cache.set(key, data, Duration.ofMinutes(5));
return data;
});
}
}| Endpoint | Descrição |
|---|---|
GET /actuator/health |
Status liveness e readiness |
GET /actuator/metrics |
Métricas Micrometer |
GET /actuator/prometheus |
Métricas para scraping Prometheus |
Em produção (spring.profiles.active=prod), logs são emitidos como JSON compatível com ELK/Loki:
{
"@timestamp": "2024-01-01T10:00:00.000Z",
"app": "boilerplate-api",
"level": "INFO",
"message": "Resolved tenant: acme",
"requestId": "550e8400-...",
"tenantId": "acme",
"userId": "9f5b6c8a-..."
}Disponível em http://localhost:8080/swagger-ui.html.
- Autenticação via JWT Bearer configurada globalmente
- Endpoints públicos (
/auth/**) marcados com@SecurityRequirements({}) - Tags agrupadas por bounded context
docker build -f docker/Dockerfile -t boilerplate-api:latest .
docker run -p 8080:8080 --env-file .env boilerplate-api:latestdocker compose -f docker/docker-compose.yml up# Criar secrets (substitua os valores)
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/secret.yaml # Edite antes com valores reais
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/hpa.yaml| Ferramenta | Descrição |
|---|---|
| ArchUnit | Valida regras de arquitetura em cada build |
| JaCoCo | Cobertura mínima de 80% (falha o build se abaixo) |
| Spotless | Formatação automática via google-java-format |
| @ConfigurationProperties + @Validated | Fail-fast se propriedades obrigatórias estiverem ausentes |
# Verificar formatação
./mvnw spotless:check
# Aplicar formatação automaticamente
./mvnw spotless:apply
# Rodar todos os testes com cobertura
./mvnw verify| Variável | Padrão | Descrição |
|---|---|---|
DB_URL |
jdbc:postgresql://localhost:5432/boilerplate |
JDBC URL do PostgreSQL |
DB_USER |
boilerplate |
Usuário do banco |
DB_PASS |
boilerplate |
Senha do banco |
REDIS_HOST |
localhost |
Host do Redis |
REDIS_PORT |
6379 |
Porta do Redis |
REDIS_PASSWORD |
(vazio) | Senha do Redis |
KAFKA_BOOTSTRAP_SERVERS |
localhost:9092 |
Endereço do Kafka |
JWT_SECRET |
(obrigatório) | Segredo HMAC-256 (≥ 32 chars) |
JWT_ACCESS_TOKEN_TTL_MS |
900000 |
TTL do access token (ms) |
JWT_REFRESH_TOKEN_TTL_MS |
604800000 |
TTL do refresh token (ms) |
MIT