br.dev.jsilveira.coresync/agents.md

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 domain e application são 100% agnósticas de framework. Elas não conhecem Spring Boot, MongoDB nem HTTP. Tudo que for infraestrutura ou framework vive exclusivamente na camada adapter.

📂 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 record do 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 IllegalArgumentException gené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/service via UUID.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)
    • @Slf4j nativo do Groovy para logging
    • record para imutabilidade

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 Service orquestra; ele não executa regras de negócio.

6. Services são POJOs

  • Classes de application/service não recebem @Service nem qualquer anotação do Spring.
  • São classes Groovy puras instanciadas como @Bean via uma classe @Configuration no 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 ↔ Entity e DomainModel ↔ DTO ocorre em classes Mapper dedicadas em adapter/out/persistence/ e adapter/in/web/, respectivamente.
  • Nunca exponha uma Entity fora 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 @ControllerAdvice global no adaptador web.
  • O @ControllerAdvice traduz exceções para respostas HTTP adequadas (ex: SessionException400 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 userId extraí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 camada application.

💻 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 @Service ou qualquer anotação do Spring
  • Injeção de dependência apenas por construtor
  • Consome somente ports/in e ports/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/out correspondente
  • 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/