mirror of
https://codeberg.org/jsilveira/br.dev.jsilveira.coresync.git
synced 2026-06-11 18:05:06 +00:00
339 lines
12 KiB
Markdown
339 lines
12 KiB
Markdown
|
|
# 🤖 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: `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 `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`
|
||
|
|
|
||
|
|
```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`
|
||
|
|
|
||
|
|
```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`
|
||
|
|
|
||
|
|
```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`
|
||
|
|
|
||
|
|
```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`
|
||
|
|
|
||
|
|
```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`
|
||
|
|
|
||
|
|
```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`
|
||
|
|
|
||
|
|
```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/`
|