Pular para o conteúdo principal

Arquitetura de pacotes

O Cotani é um monorepo modular. Cada módulo representa uma capacidade de domínio ou uma preocupação transversal, mas também precisa ter uma organização interna previsível. O objetivo desta convenção é fazer com que uma classe tenha um lugar óbvio e que a implementação não vaze para os consumidores.

Estrutura canônica

Cada módulo deve seguir esta forma:

com.cotani.<modulo>/
Cotani<Modulo>.java # fachada/factory pública do módulo
package-info.java
api/ # contratos públicos e modelos expostos
<Modulo>Service.java
<Modulo>Request.java
<Modulo>Exception.java
event/ # eventos públicos do domínio, quando existirem
spi/ # pontos de extensão públicos e deliberados
StorageContext.java
internal/ # implementação não pública do módulo
Default<Modulo>Service.java
application/ # casos de uso e orquestração, se necessário
domain/ # regras e modelos privados, se necessário
persistence/ # adapters de repositório e migrations
platform/ # Paper/Folia listeners e adapters
cache/ # cache interno, quando necessário
mapping/ # conversões entre API, storage e plataforma

Os subpacotes de internal são opcionais. Eles só devem ser criados quando houver mais de uma responsabilidade real; um módulo pequeno pode manter sua implementação diretamente em internal.

Regras de visibilidade

PacotePode conterNão pode conter
com.cotani.<modulo>fachada, factory e package-info.javaregra de negócio, repository concreto ou listener
apiinterfaces, records, value objects, exceptions, enums e eventos públicosimports de internal, impl ou detalhes de storage
spiportas de extensão estáveis para adapters e repositorieslifecycle da fachada ou estado mutável da implementação
internalservices concretos, composição e estado privadodependências de implementação de outro módulo
internal.persistenceSQL, repositories concretos, codecs e migrations privadasAPI Paper ou regra de apresentação
internal.platformlisteners e adapters Paper/Foliaacesso a storage diretamente

storage fora de internal só deve existir quando o módulo expõe deliberadamente um SPI de persistência, como migrations que o plugin consumidor precisa registrar. Caso contrário, use internal.persistence.

Nomes proibidos como organização

Não criar novos pacotes impl, util, helper, manager, common ou misc. Eles escondem a responsabilidade da classe. Use o papel real: application, persistence, platform, cache, mapping, policy ou um nome de domínio.

Classes concretas devem ser final por padrão. Implementações internas públicas por necessidade técnica devem ser marcadas com @InternalApi e nunca podem ser importadas por outro módulo.

Fluxo de dependências

consumidor
-> fachada do módulo / api
-> internal.application
-> internal.domain
-> ports em api
-> adapters em internal.persistence ou internal.platform

A direção é sempre para dentro. A API não conhece implementação; a implementação conhece a API. Um módulo também só conhece a API de outro módulo, nunca seus pacotes internal.

Paper, Folia e async

Listeners e adapters extraem IDs e dados imutáveis, delegam para um caso de uso e não carregam Player, World, Entity, Inventory ou Block para fluxos assíncronos. I/O fica em internal.persistence e a transição de volta para a thread global, de região ou de entidade fica concentrada no scheduler do módulo cotani-task.

Migração incremental

Não vamos mover todos os módulos em uma única alteração. A ordem segura é:

  1. congelar a criação de novas estruturas fora desta convenção;
  2. separar adapters de persistência e plataforma quando um módulo crescer;
  3. remover imports de implementação entre módulos;
  4. fortalecer a validação Gradle para impedir regressões;
  5. atualizar READMEs e exemplos após cada mudança estrutural.

Todos os módulos que possuíam impl foram migrados para internal. A validação Gradle agora impede que novos pacotes impl sejam introduzidos.