No artigo Java multiparadigma: records, sealed classes e pattern matching vimos como o Java ganhou records, sealed classes, pattern matching e switch expressions. Aqui o foco desloca. O que fazer com essas peças na arquitetura e quando deixar os dados orientarem o design.
Data-Oriented Programming: o sistema completo
Brian Goetz resume o núcleo do paradigma em uma frase.
"A ideia central da programação orientada a dados em Java é: modele os dados como dados, e modele o comportamento separadamente."
— Brian Goetz, Data-Oriented Programming in Java
A divisão é clara. De um lado, a forma do que existe (estrutura, variantes). De outro, o que se faz com essa forma (funções e serviços que a consomem). O modelo de dados passa a ser a bússola. Primeiro a definição dos tipos, depois o código que os processa. Em muitos domínios essa separação reduz acoplamento e deixa o fluxo de dados explícito. Em sistemas com vários consumidores para o mesmo evento (notificação, auditoria, métricas, projeção de leitura), concentrar a estrutura nos tipos e o processamento em funções que recebem esses tipos evita que cada novo caso de uso exija mudar o domínio. Um exemplo curto mostra essa divisão em ação.
public sealed interface Resultado
permits Sucesso, Falha {}
public record Sucesso(String valor) implements Resultado {}
public record Falha(String erro) implements Resultado {}
// O comportamento vive fora dos dados
public String interpretar(Resultado r) {
return switch (r) {
case Sucesso(var v) -> "OK: " + v;
case Falha(var e) -> "Erro: " + e;
};
}Os dados definem a estrutura. O switch define o que se faz com ela. Adicionar um novo consumidor (log, métrica, notificação) significa escrever outro switch sobre o mesmo tipo, sem alterar Resultado, Sucesso ou Falha.
Vale precisar o que "dados" significa aqui. Em DOP, "dados" inclui o valor das variáveis e a própria forma do tipo. Um sealed interface com três records permitidos carrega a informação "existem exatamente estas três possibilidades". O switch que desconstroi esse tipo usa essa informação para garantir exaustividade. A forma do tipo é portanto parte do dado que orienta o design. O diagrama abaixo explicita a divisão de papéis. De um lado, a modelagem (records e sealed classes) define o que existe. De outro, o processamento (pattern matching e switch expressions) define como se consome. Os dois convergem para o que a teoria dos tipos chama de Algebraic Data Types (ADTs), como desenvolvido no artigo anterior. Records representam a combinação de componentes (product types) e sealed interfaces representam a escolha entre alternativas (sum types). Juntos, formam tipos cuja estrutura e variantes são conhecidas e que podem ser desconstruídos de forma segura e exaustiva.
graph TD
A[Data-Oriented Programming] --> B[Records]
A --> C[Sealed Classes]
A --> D[Pattern Matching]
A --> E[Switch Expressions]
B --> F[Modelagem de Dados]
C --> F
D --> G[Processamento de Dados]
E --> G
F --> H[Algebraic Data Types]
G --> H
style A fill:#1a1a1a,stroke:#f5a623,color:#f5a623
style H fill:#1a1a1a,stroke:#f5a623,color:#f5a623A expressão "Data-Oriented Programming" circula há anos em outras comunidades. Em Clojure e em Lisp, dados imutáveis e funções que os transformam formam o núcleo do estilo. Yehonathan Sharvit sistematizou o tema no livro "Data-Oriented Programming" (Manning, 2022). Goetz deu ao termo um contorno preciso no Java e mostrou como records, sealed classes e pattern matching realizam o paradigma na linguagem. Quem vem de Clojure ou Scala reconhece a mesma ideia. Quem vem só de Java ganha um nome e construtos para aplicá-la de forma nativa.
O paradigma paga dividendos onde há múltiplos consumidores para o mesmo dado ou onde o dado cruza fronteiras. Em arquiteturas orientadas a eventos, um evento é publicado uma vez e lido por vários assinantes, cada um com uma interpretação (notificação, projeção, métricas, auditoria). Com eventos modelados como dados (records em sealed interface), cada assinante é um switch que decompõe o evento. O contrato do barramento é a estrutura do tipo. O mesmo vale para APIs REST ou GraphQL. Payload como dado, validação, transformação e persistência como comportamento. Contrato em record permite derivar esquema OpenAPI ou GraphQL da declaração. Handlers são funções que recebem DTO e devolvem DTO ou efeito. Os dados orientam o código e o contrato que o sistema expõe.
Considere um exemplo mais completo. Um sistema de processamento de eventos de domínio, onde diferentes tipos de eventos precisam ser roteados, validados e transformados.
public sealed interface EventoDominio
permits PedidoCriado, PedidoPago, PedidoCancelado, PedidoEnviado {
String pedidoId();
Instant ocorridoEm();
}
public record PedidoCriado(
String pedidoId, String clienteId,
List<Item> itens, Instant ocorridoEm
) implements EventoDominio {}
public record PedidoPago(
String pedidoId, String transacaoId,
BigDecimal valor, Instant ocorridoEm
) implements EventoDominio {}
public record PedidoCancelado(
String pedidoId, String motivo,
Instant ocorridoEm
) implements EventoDominio {}
public record PedidoEnviado(
String pedidoId, String rastreio,
Instant previsaoEntrega, Instant ocorridoEm
) implements EventoDominio {}
public record Item(String produtoId, int quantidade, BigDecimal preco) {}public class NotificacaoService {
public Notificacao gerar(EventoDominio evento) {
return switch (evento) {
case PedidoCriado(var id, var cliente, var itens, var quando) ->
new Notificacao(
cliente,
"Pedido %s confirmado com %d itens".formatted(id, itens.size()),
Prioridade.NORMAL
);
case PedidoPago(var id, var txId, var valor, var ocorrido) ->
new Notificacao(
buscarCliente(id),
"Pagamento de R$ %s confirmado (tx: %s)".formatted(valor, txId),
Prioridade.ALTA
);
case PedidoCancelado(var id, var motivo, var ocorrido) ->
new Notificacao(
buscarCliente(id),
"Pedido %s cancelado: %s".formatted(id, motivo),
Prioridade.ALTA
);
case PedidoEnviado(var id, var rastreio, var previsao, var ocorrido) ->
new Notificacao(
buscarCliente(id),
"Pedido %s enviado! Rastreio: %s. Previsão: %s"
.formatted(id, rastreio, previsao),
Prioridade.NORMAL
);
};
}
}Esse código tem propriedades que seriam impossíveis em Java antes dessas features. A modelagem é algébrica. O compilador conhece todas as variantes. A desconstrução é segura e os componentes são extraídos diretamente dos patterns. O processamento é exaustivo. Adicionar um novo tipo de evento força a atualização de todos os switches. Cada evento carrega exatamente o que declara, sem estado escondido. Essa transparência traz um benefício colateral para quem escreve testes. Criar um evento para um cenário de teste resume-se a instanciar o record com os valores desejados, sem mocks nem construção elaborada de objetos.
Compare isso com a abordagem clássica OO, onde cada tipo de evento teria um método gerar() polimórfico. A abordagem OO distribui o comportamento entre as classes, o que é ideal quando o comportamento pertence ao tipo. A abordagem DOP centraliza o comportamento no ponto de uso, o que é ideal quando o mesmo dado é processado de formas diferentes por consumidores diferentes (notificação, auditoria, métricas, projeção de leitura, entre outros).
A armadilha da transparência
Toda ferramenta poderosa carrega o risco de uso indevido, e records seguem essa regra. A própria especificação alerta sobre o significado preciso de "imutável".
"Uma classe record é um portador transparente e superficialmente imutável para um conjunto fixo de valores."
— JEP 395: Records
"Superficialmente imutável" é a chave. O record congela as referências, mas os objetos referenciados seguem mutáveis se forem mutáveis. Um componente que é uma List<Item> pode ser alterado externamente após a criação do record. A imutabilidade permanece apenas rasa. O exemplo abaixo ilustra o risco.
public record Pedido(String id, List<Item> itens) {}
var itens = new ArrayList<>(List.of(new Item("A", 1, BigDecimal.TEN)));
var pedido = new Pedido("001", itens);
// O record é "imutável", mas...
itens.add(new Item("B", 2, BigDecimal.ONE)); // a lista original foi mutada
pedido.itens().clear(); // o accessor retorna a referência mutávelA solução é defensiva. Copiar coleções no construtor canônico e retornar cópias não modificáveis resolve o problema.
public record Pedido(String id, List<Item> itens) {
public Pedido {
itens = List.copyOf(itens); // cópia imutável
}
}A armadilha mais sutil está na arquitetura. Quando times adotam records sem avaliar onde cada paradigma se aplica, começam a substituir entidades de domínio ricas por records transparentes. O resultado se aproxima do que Martin Fowler descreveu como "Anemic Domain Model".
"O sintoma básico de um Modelo de Domínio Anêmico é que, à primeira vista, ele parece a coisa real. Existem objetos, muitos com nomes que remetem aos substantivos do domínio, e esses objetos estão conectados com as relações e a estrutura ricas que modelos de domínio verdadeiros possuem. O problema aparece quando você observa o comportamento e percebe que quase não há comportamento nesses objetos, tornando-os pouco mais que sacos de getters e setters."
— Martin Fowler, AnemicDomainModel
Records podem acelerar essa erosão, porque tornam a criação de tipos anêmicos extremamente fácil. Na prática, times que adotaram records indiscriminadamente começaram a deslocar regras de negócio para serviços externos, criando um estilo procedural involuntário. Funções que recebem dados, transformam e devolvem novos dados, sem que nenhuma das estruturas carregue responsabilidade. Isso pode ser poderoso em sistemas orientados a eventos, onde a separação entre dados e processamento é uma escolha arquitetural consciente. Mas quando aplicado no coração do domínio, onde invariantes complexas precisam ser protegidas ao longo de toda a vida do objeto, a ausência de comportamento encapsulado se torna um problema silencioso que só aparece quando o sistema cresce.
Identidade, encapsulamento e os limites do record
Para entender onde records param de funcionar, é preciso entender o que significa identidade em um sistema. Eric Evans, no livro que deu forma ao Domain-Driven Design, colocou a distinção de forma direta.
"Muitos objetos não são fundamentalmente definidos por seus atributos, mas sim por um fio de continuidade e identidade."
— Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software
Uma ContaBancaria ilustra o ponto. Duas contas podem ter o mesmo titular, o mesmo saldo e a mesma data de abertura. Ainda assim, são contas diferentes. A identidade da conta é algo que persiste ao longo do tempo, independente dos valores que ela carrega em cada momento. O saldo muda, o titular pode mudar, mas a conta continua sendo a mesma conta.
Records definem igualdade pela estrutura. Dois records com os mesmos componentes são iguais. Essa semântica é perfeita para valores e dados em trânsito, mas produz o resultado errado para entidades.
// Record: igualdade por estrutura
public record ContaBancariaRecord(String titular, BigDecimal saldo) {}
var conta1 = new ContaBancariaRecord("Ana", new BigDecimal("1000"));
var conta2 = new ContaBancariaRecord("Ana", new BigDecimal("1000"));
conta1.equals(conta2); // true — mas são contas diferentes!O equals retorna true porque os componentes são iguais. No mundo real, essas são duas contas distintas que por acaso têm os mesmos valores naquele instante. A identidade da entidade se perde.
Além da identidade, existe o encapsulamento. O JEP 395 é explícito sobre o que records abrem mão.
"Classes record abrem mão de uma liberdade que as classes normalmente possuem, a capacidade de desacoplar a API de uma classe da sua representação interna."
— JEP 395: Records
Essa transparência é uma virtude quando o tipo existe para carregar dados. Mas quando o tipo precisa proteger regras de negócio, a exposição se torna um risco. Compare as duas abordagens para ContaBancaria.
// Classe com encapsulamento: as regras vivem dentro do tipo
public class ContaBancaria {
private final String id;
private final String titular;
private BigDecimal saldo;
private final List<Transacao> historico;
public ContaBancaria(String id, String titular, BigDecimal saldoInicial) {
this.id = id;
this.titular = titular;
this.saldo = saldoInicial;
this.historico = new ArrayList<>();
}
public void debitar(BigDecimal valor) {
if (valor.compareTo(saldo) > 0) {
throw new SaldoInsuficienteException(id, saldo, valor);
}
saldo = saldo.subtract(valor);
historico.add(new Transacao(TipoTransacao.DEBITO, valor, Instant.now()));
}
public void creditar(BigDecimal valor) {
saldo = saldo.add(valor);
historico.add(new Transacao(TipoTransacao.CREDITO, valor, Instant.now()));
}
public BigDecimal consultarSaldo() {
return saldo;
}
@Override
public boolean equals(Object o) {
return o instanceof ContaBancaria c && id.equals(c.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
}O saldo só muda através de debitar() e creditar(). A invariante "saldo nunca negativo sem autorização" é protegida pelo próprio tipo. O equals compara pelo id, preservando a identidade da entidade. Qualquer consumidor que receba uma ContaBancaria opera sob essas garantias sem precisar conhecê-las.
Agora a mesma tentativa com record.
// Record: tudo exposto, nenhuma garantia
public record ContaBancariaRecord(
String id, String titular,
BigDecimal saldo, List<Transacao> historico
) {}
var conta = new ContaBancariaRecord(
"001", "Ana", new BigDecimal("1000"), new ArrayList<>()
);
// Qualquer código pode "debitar" sem validação
var novaConta = new ContaBancariaRecord(
conta.id(), conta.titular(),
conta.saldo().subtract(new BigDecimal("5000")), // saldo negativo, sem barreira
conta.historico()
);O record expõe todos os componentes via acessores. Qualquer trecho de código pode reconstruir uma nova instância com saldo negativo, sem passar por nenhuma regra. A invariante que a classe protegia desaparece. Goetz aponta para essa zona de forma precisa.
"A OO está no seu melhor quando define e defende fronteiras, fronteiras de manutenção, fronteiras de versionamento, fronteiras de encapsulamento, fronteiras de compilação, fronteiras de compatibilidade, fronteiras de segurança, etc."
— Brian Goetz, Data-Oriented Programming in Java
Onde essas fronteiras precisam existir, o record trabalha contra o design. A transparência que o torna ideal para dados em trânsito é a mesma que remove as barreiras de proteção quando aplicada no domínio.
O diagrama abaixo mapeia essa separação. Entidades e aggregates vivem no núcleo, protegidos por encapsulamento. Records habitam as fronteiras, onde a transparência é desejável.
graph LR
subgraph Fronteira Externa
A[API REST] --> B[Request DTO record]
Z[Response DTO record] --> A
end
subgraph Application Layer
B --> C[Command record]
C --> D[Application Service]
D --> E[Domain Event record]
D --> Z
end
subgraph Núcleo de Domínio
D --> F[Entity Aggregate class]
F --> G[Value Object record]
end
subgraph Fronteira de Saída
E --> H[Message Broker]
D --> I[Repository]
end
style F fill:#1a1a1a,stroke:#f5a623,color:#f5a623
style G fill:#1a1a1a,stroke:#f5a623,color:#f5a623O custo de evoluir sem copy
Records em Java são imutáveis. Para produzir um novo valor a partir de um existente, é preciso reconstruir o record inteiro, passando todos os componentes manualmente.
public record Endereco(
String rua, String numero, String complemento,
String bairro, String cidade, String estado, String cep
) {}
var original = new Endereco(
"Rua das Flores", "42", "Apto 3",
"Centro", "São Paulo", "SP", "01001-000"
);
// Para mudar apenas o complemento, todos os outros componentes são repetidos
var atualizado = new Endereco(
original.rua(), original.numero(), "Apto 7",
original.bairro(), original.cidade(), original.estado(), original.cep()
);Em Kotlin, data classes oferecem o método copy() que resolve esse problema de forma concisa.
data class Endereco(
val rua: String, val numero: String, val complemento: String,
val bairro: String, val cidade: String, val estado: String, val cep: String
)
val atualizado = original.copy(complemento = "Apto 7")A documentação oficial do Kotlin descreve o propósito de forma direta.
"Use a função copy() para copiar um objeto, permitindo alterar algumas de suas propriedades enquanto mantém o restante inalterado."
— Kotlin Documentation, Data Classes
Java optou por não incluir copy() nos records. A discussão na lista amber-spec-experts do OpenJDK tratou do tema, e a decisão foi consciente. Com poucos componentes, a reconstrução manual é aceitável. Com muitos, o custo escala e o código fica verboso. Esse é um ponto que o engenheiro precisa considerar ao modelar records com muitos componentes. Se o tipo vai ser frequentemente "evoluído" para novos valores com pequenas variações, a ausência de copy() pesa.
Value Objects: onde o record encaixa com perfeição
Se entidades dependem de identidade e encapsulamento, value objects vivem no polo oposto. A identidade de um value object é definida inteiramente pelos seus componentes. Dois objetos Dinheiro com o mesmo valor e a mesma moeda representam, por definição, o mesmo conceito.
public record Dinheiro(BigDecimal valor, String moeda) {
public Dinheiro {
if (valor == null || valor.compareTo(BigDecimal.ZERO) < 0)
throw new IllegalArgumentException("Valor não pode ser nulo ou negativo");
if (moeda == null || moeda.isBlank())
throw new IllegalArgumentException("Moeda não pode ser vazia");
}
public Dinheiro somar(Dinheiro outro) {
if (!this.moeda.equals(outro.moeda))
throw new IllegalArgumentException(
"Não é possível somar moedas diferentes: %s e %s"
.formatted(this.moeda, outro.moeda));
return new Dinheiro(this.valor.add(outro.valor), this.moeda);
}
}public record Coordenada(double latitude, double longitude) {
public Coordenada {
if (latitude < -90 || latitude > 90)
throw new IllegalArgumentException("Latitude fora do intervalo válido");
if (longitude < -180 || longitude > 180)
throw new IllegalArgumentException("Longitude fora do intervalo válido");
}
}Nesses tipos, a igualdade estrutural dos records faz exatamente o que se espera. new Dinheiro(new BigDecimal("100"), "BRL") é igual a outro Dinheiro com os mesmos valores, porque representam a mesma coisa. A validação no construtor canônico garante que nenhuma instância inválida exista. O record carrega o dado e a garantia de validade, sem precisar esconder nada.
Mudança de estado versus evolução de estado
Uma forma de fixar a decisão entre classe e record é distinguir mudança de estado de evolução de estado.
Na mudança de estado, o mesmo objeto é alterado ao longo do tempo. A referência permanece, mas o conteúdo interno muda. A conta bancária recebe débito e crédito por métodos que atualizam seu saldo e seu histórico no próprio objeto. O ciclo de vida inclui carregar, modificar e persistir a mesma instância.
Na evolução de estado, não se altera o objeto. Produz-se um novo valor a partir do anterior. Um evento de domínio não "muda". Ele ocorre e é representado por um record imutável. Uma resposta de API não "atualiza" um DTO. Cada resposta é um novo valor.
graph TD
subgraph "Mudança de estado (classe)"
E1[ContaBancaria id=001
saldo: 1000] -->|debitar 200| E2[ContaBancaria id=001
saldo: 800]
E2 -->|creditar 500| E3[ContaBancaria id=001
saldo: 1300]
end
subgraph "Evolução de estado (record)"
R1[PedidoCriado] --> R2[PedidoPago]
R2 --> R3[PedidoEnviado]
end
style E1 fill:#1a1a1a,stroke:#f5a623,color:#f5a623
style E2 fill:#1a1a1a,stroke:#f5a623,color:#f5a623
style E3 fill:#1a1a1a,stroke:#f5a623,color:#f5a623Na mudança de estado, a mesma identidade atravessa o tempo. Na evolução de estado, cada instância é um fato imutável que se sucede ao anterior. Se o tipo representa algo que "vive" no tempo e acumula alterações na mesma identidade, a classe com encapsulamento segue adequada. Se o tipo representa um fato, um comando, um valor ou um contrato de passagem entre camadas, o record encaixa.
Fronteiras arquiteturais
Records brilham nas fronteiras do sistema. DTOs de entrada e saída, comandos, queries, eventos de domínio e projeções de leitura são todos tipos que existem para carregar dados entre contextos. Nessas zonas, a transparência é uma virtude. Quando um record atravessa uma fila, uma API REST ou um barramento de eventos, seu significado não depende de código escondido. A estrutura define a identidade do dado. Essa mesma estrutura mapeia de forma direta para JSON, para contratos OpenAPI ou para o payload de uma mensagem. Bibliotecas de serialização conseguem inspecionar os componentes do record e gerar esquemas, validar entrada e produzir documentação sem código adicional.
A serialização de records reforça essa filosofia. O mecanismo histórico de serialização em Java (java.io.Serializable) dependia de um número de versão, o serialVersionUID, para garantir compatibilidade entre quem serializa e quem desserializa. Na prática, isso gerou fragilidade. Esquecer de declarar o UID fazia com que uma simples refatoração quebrasse compatibilidade. Declarar e depois alterar a classe de forma incompatível mantendo o mesmo UID produzia bugs silenciosos. Records eliminam esse problema. O formato serializado é derivado diretamente dos componentes do record, sem identificador de versão separado. A estrutura é o contrato. Quem lê a declaração do record sabe exatamente o que trafega.
O hibridismo consciente
Goetz coloca a coexistência dos paradigmas de forma precisa.
"As técnicas da OOP e da programação orientada a dados não estão em conflito. São ferramentas diferentes para granularidades e situações diferentes. Podemos combiná-las livremente conforme julgarmos adequado."
— Brian Goetz, Data-Oriented Programming in Java
A maturidade no uso de Java moderno aparece quando o engenheiro reconhece que orientação a objetos e orientação a dados se complementam. Entidades ricas concentram comportamento onde a identidade e as invariantes do domínio pedem proteção. Records aparecem nas fronteiras, onde dados transitam entre contextos e a clareza estrutural importa mais que encapsulação. Sealed classes entram quando o domínio exige um conjunto finito e conhecido de variantes. Pattern matching ganha espaço onde a interpretação dos dados varia conforme o contexto.
Quem trabalha com código legado em Java 8 ou 11 pode adotar records e sealed classes de forma gradual. A estratégia que mais reduz risco é começar pelas bordas do sistema. Novos DTOs de API, novos eventos de domínio, novos comandos ou queries passam a ser modelados como records. As entidades e aggregates existentes permanecem como classes. Com o tempo, value objects que hoje são classes imutáveis podem ser migrados para records, e hierarquias que representam escolhas fechadas (como resultados de operação ou tipos de evento) podem ser seladas. A compatibilidade com serialização antiga, reflexão e ferramentas que inspecionam tipos costuma ser preservada, pois records são classes no bytecode. O que muda é a semântica e a concisão no código fonte.
A virada multiparadigma de Java acompanha uma mudança no tipo de software que o mundo passou a exigir. Sistemas distribuídos, arquiteturas orientadas a eventos, processamento massivo de dados e APIs como contratos entre times e organizações demandam formas diferentes de expressar intenção. Quando a linguagem oferece apenas um paradigma, o engenheiro acaba distorcendo o problema para caber na solução. Com mais de um paradigma disponível, a solução pode se adaptar ao problema, e cada contexto escolhe o estilo que melhor o descreve.
A evolução de Java, de uma linguagem puramente orientada a objetos para um ecossistema multiparadigma completo, reflete uma resposta madura às pressões do software moderno. A linguagem incorporou novos modos de expressão sem abandonar o que já funcionava. O que define o resultado é o uso consciente de cada ferramenta, mais que a quantidade disponível. Onde a programação orientada a dados se aplica, isso significa deixar os dados orientarem o design. Primeiro a estrutura e as variantes, depois o código que as consome. Em toda evolução genuína, o ganho está em saber quando aplicar qual paradigma, e em deixar que o problema guie a escolha.
Conclusão
No artigo anterior, acompanhamos o Java incorporando lambdas, records, sealed classes, pattern matching e switch expressions. Cada feature respondia a uma limitação real da linguagem. Neste artigo, vimos o que acontece quando essas peças se organizam em um paradigma coerente e onde esse paradigma encontra seus limites. A separação entre dados e comportamento funciona quando o dado cruza fronteiras. O encapsulamento funciona quando o domínio precisa de proteção. Identidade, invariantes e ciclo de vida pedem classes com comportamento. Transporte, contratos e evolução imutável pedem records. O ponto de maturidade é saber onde cada um se aplica.
A linguagem oferece mais repertório do que a maioria dos projetos utiliza hoje. Records, sealed classes e pattern matching estão disponíveis desde o Java 21, mas a adoção depende menos da ferramenta e mais de como as equipes pensam sobre design. O ganho real não vem de substituir classes por records ou de adotar pattern matching em todo switch. Vem de reconhecer a natureza do tipo que se está modelando e de escolher o construto que expressa essa natureza com mais fidelidade.
"Quando estamos modelando entidades complexas, as técnicas de OO têm muito a nos oferecer. Mas quando estamos construindo serviços simples que processam dados simples e ad-hoc, as técnicas da programação orientada a dados podem nos oferecer um caminho mais direto."
— Brian Goetz, Data-Oriented Programming in Java
A melhor ferramenta não é a mais nova. É a que o engenheiro sabe quando usar. E saber quando usar exige entender o que cada paradigma protege, o que cada paradigma expõe, e o que o problema à frente está pedindo.
Bibliografia
Brian Goetz. Data-Oriented Programming in Java. InfoQ, 2022.
Brian Goetz. Data-Oriented Programming in Java (v1.1). Inside.java, 2024.
Eric Evans. Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2003.
Martin Fowler. AnemicDomainModel. martinfowler.com, 2003.
Yehonathan Sharvit. Data-Oriented Programming. Manning, 2022.
OpenJDK. JEP 395: Records. 2021.
OpenJDK. JEP 409: Sealed Classes. 2021.
OpenJDK. JEP 440: Record Patterns. 2023.
OpenJDK. JEP 441: Pattern Matching for switch. 2023.
Kotlin Documentation. Data Classes. kotlinlang.org.