12 KiB
🤖 CoreSync — Agent Guidelines
Este documento define a arquitetura, as regras de negócio e os padrões de código obrigatórios para o desenvolvimento do backend do CoreSync. Todo código gerado ou modificado deve obedecer estritamente às diretrizes abaixo.
🛠 Stack Tecnológico
| Camada | Tecnologia |
|---|---|
| Linguagem | Groovy (Closures, Records, sintaxe enxuta) |
| Runtime | Java 26 (toolchain configurado no Gradle) |
| Framework | Spring Boot (Web, Security, Data MongoDB) |
| Banco de Dados | MongoDB |
| Cliente | React Native — comunicação exclusiva via JSON/REST + JWT stateless |
🏛 Arquitetura: Hexagonal (Ports & Adapters) + DDD
O sistema é um Monolito Modular construído sobre Arquitetura Hexagonal. O objetivo é o isolamento absoluto da Regra de Negócio.
Regra de Ouro
As camadas
domaineapplicationsão 100% agnósticas de framework. Elas não conhecem Spring Boot, MongoDB nem HTTP. Tudo que for infraestrutura ou framework vive exclusivamente na camadaadapter.
📂 Estrutura de Pacotes Obrigatória
Os pacotes são organizados por módulo de negócio (ex: user, workout, session). Dentro de cada módulo:
src/main/groovy/br/dev/jsilveira/coresync/[modulo]/
├── domain/
│ ├── model/ # Records Groovy — Agregados, Entidades e Value Objects
│ └── exception/ # Exceções de negócio do módulo
│
├── application/
│ ├── port/
│ │ ├── in/ # Interfaces de entrada (Casos de Uso chamados pela API)
│ │ └── out/ # Interfaces de saída (o que o domínio precisa do mundo externo)
│ └── service/ # Implementa ports/in; consome ports/out
│
├── adapter/
│ ├── in/web/ # Controllers REST + DTOs de entrada/saída
│ └── out/persistence/ # Spring Data Repositories, Entities MongoDB, Mappers
│
└── config/ # Classes @Configuration com definições de @Bean
📜 Regras de Código
1. Modelagem com Records (Imutabilidade)
- Todas as Entidades, Agregados e Value Objects do domínio devem ser
recorddo Groovy. - O estado nunca é mutado diretamente. Toda transição de estado produz uma nova instância do record (padrão copy-on-write).
2. Validação no Construtor Compacto
- Toda validação de invariantes de negócio ocorre no construtor compacto do record.
- É proibido instanciar um objeto de domínio em estado inválido (IDs nulos, strings vazias, datas inconsistentes, dependências ausentes).
- Violations devem lançar a exceção de negócio específica do módulo (nunca
IllegalArgumentExceptiongenérica).
3. Identificadores (UUID)
- O banco de dados não gera IDs. Cada entidade possui seu próprio
UUID id. - A geração do ID ocorre na camada
application/serviceviaUUID.randomUUID(), antes do envio ao adaptador de persistência. - Referências entre Aggregate Roots são feitas exclusivamente por ID — nunca aninhe objetos raiz dentro de outros.
4. Zero Lombok
- Lombok é proibido. Use os recursos nativos do Groovy:
- Construtores de mapa (
new Foo(bar: "x")) - Propriedades automáticas (
@groovy.transform.CompileStatic) @Slf4jnativo do Groovy para loggingrecordpara imutabilidade
- Construtores de mapa (
5. Domínio Rico (Rich Domain)
- O domínio não deve ser anêmico. Lógicas de cálculo, transições de estado, formatações e validações pertinentes à entidade ficam dentro do próprio record.
- O
Serviceorquestra; ele não executa regras de negócio.
6. Services são POJOs
- Classes de
application/servicenão recebem@Servicenem qualquer anotação do Spring. - São classes Groovy puras instanciadas como
@Beanvia uma classe@Configurationno módulo. - Dependências injetadas exclusivamente pelo construtor.
7. Nomenclatura
| Artefato | Convenção |
|---|---|
| Porta de entrada | [Ação][Entidade]UseCase (ex: FinishSessionUseCase) |
| Porta de saída — leitura | Load[Entidade]Port |
| Porta de saída — escrita | Save[Entidade]Port |
| Porta de saída — remoção | Delete[Entidade]Port |
| Serviço | [Ação][Entidade]Service |
| Controller | [Entidade]Controller |
| Entity (Mongo) | [Entidade]Entity |
| DTO de entrada | [Entidade]Request |
| DTO de saída | [Entidade]Response |
8. Mapeamento entre Camadas
- Conversão entre
DomainModel ↔ EntityeDomainModel ↔ DTOocorre em classes Mapper dedicadas emadapter/out/persistence/eadapter/in/web/, respectivamente. - Nunca exponha uma
Entityfora do adaptador de persistência. - Nunca exponha um objeto de domínio diretamente em uma resposta HTTP.
9. Tratamento de Erros
- Exceções de negócio definidas em
domain/exception/são capturadas por um@ControllerAdviceglobal no adaptador web. - O
@ControllerAdvicetraduz exceções para respostas HTTP adequadas (ex:SessionException→400 Bad Request). - O domínio nunca conhece códigos HTTP.
10. Segurança e Autenticação
- Toda autenticação é stateless via JWT.
- O token é validado no adaptador (
adapter/in/web/) antes de qualquer chamada ao caso de uso. - O
userIdextraído do token deve ser passado explicitamente como parâmetro ao caso de uso — nunca acessado diretamente de um contexto de segurança dentro da camadaapplication.
💻 Exemplos de Código
Modelo de Domínio — record com Construtor Compacto
Local: session/domain/model/UserWorkoutSession.groovy
package br.dev.jsilveira.coresync.session.domain.model
import br.dev.jsilveira.coresync.session.domain.exception.SessionException
import java.time.LocalDateTime
import java.util.UUID
record UserWorkoutSession(
UUID id,
UUID userId,
UUID workoutDayId,
LocalDateTime startedAt,
LocalDateTime completedAt
) {
// Construtor compacto: validação de invariantes de negócio
public UserWorkoutSession {
if (!id) throw new SessionException("O ID não pode ser nulo")
if (!userId) throw new SessionException("O ID do usuário é obrigatório")
if (!workoutDayId) throw new SessionException("O ID do treino é obrigatório")
if (!startedAt) throw new SessionException("A data de início é obrigatória")
if (completedAt != null && completedAt.isBefore(startedAt)) {
throw new SessionException("A data de conclusão não pode ser anterior à data de início")
}
}
// Domínio Rico: transição de estado gera uma nova instância imutável
UserWorkoutSession finish(LocalDateTime time = LocalDateTime.now()) {
if (completedAt != null) throw new SessionException("A sessão já foi finalizada")
return new UserWorkoutSession(id, userId, workoutDayId, startedAt, time)
}
boolean isFinished() {
return completedAt != null
}
}
Porta de Entrada (API)
Local: session/application/port/in/FinishSessionUseCase.groovy
package br.dev.jsilveira.coresync.session.application.port.in
import java.util.UUID
interface FinishSessionUseCase {
void execute(UUID sessionId, UUID requestingUserId)
}
Porta de Saída (SPI)
Local: session/application/port/out/SaveSessionPort.groovy
package br.dev.jsilveira.coresync.session.application.port.out
import br.dev.jsilveira.coresync.session.domain.model.UserWorkoutSession
// O domínio declara o contrato; a implementação vive no adaptador
interface SaveSessionPort {
void save(UserWorkoutSession session)
}
Local: session/application/port/out/LoadSessionPort.groovy
package br.dev.jsilveira.coresync.session.application.port.out
import br.dev.jsilveira.coresync.session.domain.model.UserWorkoutSession
import java.util.UUID
interface LoadSessionPort {
UserWorkoutSession load(UUID sessionId)
}
Serviço (Caso de Uso) — POJO Groovy
Local: session/application/service/FinishWorkoutSessionService.groovy
package br.dev.jsilveira.coresync.session.application.service
import br.dev.jsilveira.coresync.session.application.port.in.FinishSessionUseCase
import br.dev.jsilveira.coresync.session.application.port.out.LoadSessionPort
import br.dev.jsilveira.coresync.session.application.port.out.SaveSessionPort
import br.dev.jsilveira.coresync.session.domain.exception.SessionException
import java.util.UUID
// Sem @Service — instanciado via @Bean em SessionConfig
class FinishWorkoutSessionService implements FinishSessionUseCase {
private final LoadSessionPort loadPort
private final SaveSessionPort savePort
FinishWorkoutSessionService(LoadSessionPort loadPort, SaveSessionPort savePort) {
this.loadPort = loadPort
this.savePort = savePort
}
@Override
void execute(UUID sessionId, UUID requestingUserId) {
def session = loadPort.load(sessionId)
// Autorização de negócio: o usuário só pode finalizar a própria sessão
if (session.userId() != requestingUserId) {
throw new SessionException("Usuário não autorizado a finalizar esta sessão")
}
// Regra de negócio encapsulada no domínio
def finishedSession = session.finish()
savePort.save(finishedSession)
}
}
Entity de Banco (Adaptador de Persistência)
Local: session/adapter/out/persistence/entity/SessionEntity.groovy
package br.dev.jsilveira.coresync.session.adapter.out.persistence.entity
import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.mapping.Document
import java.time.LocalDateTime
import java.util.UUID
// Anotações de framework são permitidas apenas na camada adapter
@Document(collection = "workout_sessions")
record SessionEntity(
@Id UUID id,
UUID userId,
UUID workoutDayId,
LocalDateTime startedAt,
LocalDateTime completedAt
) {}
Configuração do Módulo (Bean Wiring)
Local: session/config/SessionConfig.groovy
package br.dev.jsilveira.coresync.session.config
import br.dev.jsilveira.coresync.session.application.port.out.LoadSessionPort
import br.dev.jsilveira.coresync.session.application.port.out.SaveSessionPort
import br.dev.jsilveira.coresync.session.application.service.FinishWorkoutSessionService
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class SessionConfig {
@Bean
FinishWorkoutSessionService finishWorkoutSessionService(
LoadSessionPort loadPort,
SaveSessionPort savePort
) {
return new FinishWorkoutSessionService(loadPort, savePort)
}
}
✅ Checklist por Artefato
Antes de submeter qualquer código, verifique:
Domain Model (record)
- Validações completas no construtor compacto
- Sem anotações de Spring ou Mongo
- Transições de estado retornam nova instância (imutabilidade)
- Lógica de negócio pertinente está no record, não no service
Application Service
- Sem
@Serviceou qualquer anotação do Spring - Injeção de dependência apenas por construtor
- Consome somente
ports/ineports/out - Não contém regras de negócio (delega ao domínio)
Adapter (Controller / Repository)
- Controller recebe e retorna apenas DTOs (
*Request/*Response) - Mapeamento domínio ↔ DTO em Mapper dedicado
- Repository implementa a interface
port/outcorrespondente - Mapeamento domínio ↔ Entity em Mapper dedicado
Geral
- Nenhum import do Lombok em qualquer camada
- IDs gerados na camada
application, nunca no banco - Referências entre Aggregate Roots feitas apenas por UUID
- Exceções de negócio definidas em
domain/exception/