SOLID na Pratica: Next.js e Spring Boot
Este material e um estudo aprofundado sobre como aplicar SOLID em sistemas reais, indo alem da definicao teorica. O foco e reduzir acoplamento, facilitar mudancas e aumentar testabilidade.
O que e SOLID
SOLID e um conjunto de 5 principios de design orientado a objetos:
- SRP (Single Responsibility Principle): uma classe/modulo deve ter um unico motivo para mudar.
- OCP (Open/Closed Principle): entidades devem estar abertas para extensao e fechadas para modificacao.
- LSP (Liskov Substitution Principle): subtipos devem poder substituir o tipo base sem quebrar comportamento.
- ISP (Interface Segregation Principle): prefira interfaces pequenas e especificas.
- DIP (Dependency Inversion Principle): dependa de abstracoes, nao de implementacoes concretas.
Em TypeScript e Java, SOLID nao significa "muitas classes", e sim fronteiras claras entre regras de negocio e detalhes de framework.
Por que SOLID importa em arquitetura
Quando projetos crescem sem SOLID, os sintomas aparecem rapido:
- Regras de negocio espalhadas em controllers/handlers.
- Mudancas pequenas exigindo alteracoes em varios pontos.
- Testes lentos, porque tudo depende de banco, fila, cache ou API externa.
- Alto risco de regressao por falta de isolamento.
Com SOLID:
- use cases ficam previsiveis;
- infraestrutura vira detalhe substituivel;
- testes de regra rodam sem framework;
- evolucao para microservicos/modular monolith fica mais simples.
Como estruturar SOLID na pratica
Uma forma pragmatica e usar 4 blocos:
- Domain: entidades, regras, invariantes.
- Application: casos de uso (orquestracao de regras).
- Ports: contratos de entrada/saida (interfaces).
- Adapters: implementacoes concretas (DB, HTTP, cache, mensageria).
Mapeamento para Next.js
app/api/*ouserver actions: adapter de entrada HTTP/UI.src/core/application/*: casos de uso.src/core/domain/*: entidades e regras.src/core/ports/*: interfaces de repositorio/servicos.src/infra/*: Prisma, Redis, fila, gateways externos.
Mapeamento para Spring Boot
controller: adapter de entrada.application/usecase: casos de uso.domain: entidades e servicos de dominio.application/port/ineapplication/port/out: contratos.infrastructure: JPA, clients HTTP, mensageria.
S - Single Responsibility Principle (SRP)
Regra pratica
Se um modulo muda por mais de um motivo (negocio + persistencia + transporte), SRP esta quebrado.
Exemplo ruim (Next.js)
// app/api/orders/route.ts
// Mistura validacao, regra de negocio, persistencia e notificacao
export async function POST(req: Request) {
const body = await req.json()
if (!body.items?.length) return Response.json({ error: "invalid" }, { status: 400 })
const total = body.items.reduce((sum: number, item: any) => sum + item.price, 0)
const order = await prisma.order.create({ data: { total } })
await emailClient.send({ to: body.email, subject: "Pedido criado" })
return Response.json(order)
}Exemplo melhor (Next.js)
// app/api/orders/route.ts
import { createOrderUseCase } from "@/core/application/create-order"
import { prismaOrderRepository } from "@/infra/repositories/prisma-order-repository"
import { resendEmailGateway } from "@/infra/gateways/resend-email-gateway"
export async function POST(req: Request) {
const input = await req.json()
const useCase = createOrderUseCase(prismaOrderRepository, resendEmailGateway)
const output = await useCase.execute(input)
return Response.json(output, { status: 201 })
}SRP aqui: route adapta HTTP, use case orquestra negocio, repositorio persiste, gateway notifica.
Exemplo melhor (Spring Boot)
@RestController
@RequestMapping("/orders")
class OrderController {
private final CreateOrderUseCase useCase;
OrderController(CreateOrderUseCase useCase) {
this.useCase = useCase;
}
@PostMapping
ResponseEntity<OrderOutput> create(@RequestBody CreateOrderInput input) {
return ResponseEntity.status(201).body(useCase.execute(input));
}
}O - Open/Closed Principle (OCP)
Regra pratica
Adicionar comportamento novo sem editar codigo estavel.
Caso comum: descontos por tipo de cliente
Em vez de if/else gigante, use estrategia:
export interface DiscountPolicy {
supports(type: string): boolean
calculate(total: number): number
}
export class VipDiscount implements DiscountPolicy {
supports(type: string) { return type === "VIP" }
calculate(total: number) { return total * 0.85 }
}
export class RegularDiscount implements DiscountPolicy {
supports(type: string) { return type === "REGULAR" }
calculate(total: number) { return total * 0.95 }
}
export class DiscountService {
constructor(private readonly policies: DiscountPolicy[]) {}
apply(customerType: string, total: number): number {
const policy = this.policies.find((p) => p.supports(customerType))
return policy ? policy.calculate(total) : total
}
}Novo tipo de cliente: criar nova policy, sem alterar DiscountService.
No Spring Boot, o mesmo padrao funciona com injecao de List<DiscountPolicy>.
L - Liskov Substitution Principle (LSP)
Regra pratica
Se uma implementacao forcada muda pre-condicoes, pos-condicoes ou excecoes esperadas, ela quebra LSP.
Exemplo classico
PaymentGateway.charge(...)promete cobrar.SandboxPaymentGatewaynao pode "fingir sucesso" sem contrato explicito.
Boa pratica:
- definir contrato com semantica clara;
- separar ambiente real/sandbox por configuracao;
- testar contrato com suite compartilhada (contract tests).
I - Interface Segregation Principle (ISP)
Regra pratica
Interfaces gordas forcam implementacoes a conhecer detalhes que nao usam.
Exemplo
Ruim:
interface UserRepository {
save(user: User): Promise<void>
findById(id: string): Promise<User | null>
exportCsv(): Promise<string>
sendMarketingEmail(): Promise<void>
}Melhor:
interface UserWriter {
save(user: User): Promise<void>
}
interface UserReader {
findById(id: string): Promise<User | null>
}No Spring Boot: separar UserQueryPort de UserCommandPort.
D - Dependency Inversion Principle (DIP)
Regra pratica
Casos de uso dependem de portas, nao de bibliotecas/framework.
Exemplo em Next.js
export interface OrderRepository {
save(order: Order): Promise<void>
}
export interface NotificationGateway {
notifyOrderCreated(email: string): Promise<void>
}
export function createOrderUseCase(
orderRepository: OrderRepository,
notificationGateway: NotificationGateway,
) {
return {
async execute(input: CreateOrderInput) {
const order = Order.create(input)
await orderRepository.save(order)
await notificationGateway.notifyOrderCreated(input.email)
return { id: order.id }
},
}
}Esse use case roda em teste sem Prisma, sem HTTP, sem Next.js.
Exemplo em Spring Boot
public interface OrderRepositoryPort {
void save(Order order);
}
public interface NotificationPort {
void notifyOrderCreated(String email);
}
@Service
public class CreateOrderUseCase {
private final OrderRepositoryPort orderRepository;
private final NotificationPort notification;
public CreateOrderUseCase(OrderRepositoryPort orderRepository, NotificationPort notification) {
this.orderRepository = orderRepository;
this.notification = notification;
}
public OrderOutput execute(CreateOrderInput input) {
Order order = Order.create(input);
orderRepository.save(order);
notification.notifyOrderCreated(input.email());
return new OrderOutput(order.getId());
}
}Exemplo de estrutura de pastas
Next.js (App Router + TypeScript)
src/
app/
api/orders/route.ts
core/
domain/order.ts
application/create-order.ts
ports/order-repository.ts
ports/notification-gateway.ts
infra/
repositories/prisma-order-repository.ts
gateways/resend-notification-gateway.tsSpring Boot (Hexagonal simplificada)
src/main/java/com/acme/orders/
domain/Order.java
application/usecase/CreateOrderUseCase.java
application/port/in/CreateOrderInputPort.java
application/port/out/OrderRepositoryPort.java
infrastructure/web/OrderController.java
infrastructure/persistence/JpaOrderRepositoryAdapter.java
infrastructure/notification/EmailNotificationAdapter.javaRoteiro de adocao em projeto legado
- Identifique um fluxo de negocio critico (ex.: criar pedido).
- Extraia um use case unico para este fluxo.
- Defina portas de saida (repositorio, fila, email).
- Implemente adapters sem alterar regra de negocio.
- Cubra o use case com testes unitarios de regra.
- Repita para proximos fluxos.
Nao tente "solidificar" tudo de uma vez. Evolucao incremental reduz risco.
Armadilhas comuns
- Criar interfaces para tudo sem necessidade real.
- Confundir arquitetura limpa com excesso de camadas.
- Colocar validacao de regra de negocio apenas no controller.
- Fazer mocks demais e ignorar contract/integration tests.
- Tratar SOLID como checklist, e nao como criterio de design.
Checklist rapido de revisao
- O caso de uso executa sem framework?
- Mudanca de banco/API externa exige alterar regra de negocio?
- Ha
if/elsecrescendo para variacoes de comportamento? - Interfaces estao pequenas e focadas?
- Existe teste de contrato para adapters criticos?
Se 3 ou mais respostas forem "nao", existe alto ganho ao aplicar SOLID.
Recursos recomendados
Canais no YouTube
- Continuous Delivery - design, arquitetura e clean code.
- Amigoscode - Spring Boot, design orientado a dominio e boas praticas.
- Java Brains - fundamentos de arquitetura e Spring.
- Vercel - arquitetura moderna no ecossistema Next.js.
- Fireship - explicacoes objetivas de conceitos e stack web.
Videos recomendados
- SOLID Design Principles Explained (Continuous Delivery).
- SOLID Principles in Java (Java Brains).
- SOLID Principles Tutorial (Amigoscode).
Artigos e documentacao
- The Principles of OOD (Uncle Bob) - origem e contexto dos principios.
- SOLID Principles: Java guide (Baeldung) - exemplos praticos em Java.
- SOLID Principles in JavaScript (LogRocket) - aplicacao no ecossistema JS/TS.
- Next.js App Router docs - estrutura recomendada para separar responsabilidades.
- Spring Framework docs - DI no core do Spring.
Livros
- Clean Architecture - Robert C. Martin.
- Clean Code - Robert C. Martin.
- Agile Software Development: Principles, Patterns, and Practices - Robert C. Martin.
- Spring in Action, 6th Edition - Craig Walls.
- Implementing Domain-Driven Design - Vaughn Vernon.
Conclusao
SOLID funciona melhor quando aplicado a fluxos reais de negocio, com fronteiras claras entre regra, contrato e infraestrutura. Em Next.js e Spring Boot, o ganho vem de deixar framework como detalhe e manter o nucleo da aplicacao simples, testavel e evolutivo.