Para uma plataforma de ensino como Hotmart/Kiwify, eu iria de abordagem pragmática, mas aplicando os conceitos mais valiosos do DDD sem o dogmatismo. Aqui está o porquê:
Use a Clean Architecture (ou Hexagonal) como guarda-chuva, mas aplique conceitos seletivos do DDD onde o negócio é mais crítico.
Já comece separando sua aplicação em contextos. Não precisa ser microserviços, pode ser módulos dentro do mesmo projeto:
src/
├── contexts/
│ ├── catalog/ (Cursos, módulos, aulas)
│ │ ├── application/
│ │ ├── domain/
│ │ └── infrastructure/
│ │
│ ├── sales/ (Pedidos, carrinho, checkout)
│ │ ├── application/
│ │ ├── domain/
│ │ └── infrastructure/
│ │
│ ├── payment/ (Transações, gateways, split de comissão)
│ │ ├── application/
│ │ ├── domain/
│ │ └── infrastructure/
│ │
│ ├── marketing/ (Afiliados, cupons, comissões)
│ │ ├── application/
│ │ ├── domain/
│ │ └── infrastructure/
│ │
│ └── community/ (Fórum, anotações, progresso)
│ ├── application/
│ ├── domain/
│ └── infrastructure/Não faça todas as entidades anêmicas. Crie modelos ricos nos contextos críticos:
// BOM (Domain Model Rico - Contexto de Vendas)
class Pedido {
constructor(
private itens: ItemPedido[],
private cupom?: Cupom
) {}
aplicarCupom(cupom: Cupom): void {
if (!cupom.estaValido()) {
throw new ErroCupomInvalido();
}
this.cupom = cupom;
this.recalcularTotal();
}
adicionarItem(produto: Produto, quantidade: number): void {
// Regra de negócio DENTRO da entidade
if (this.itens.length >= 10) {
throw new ErroLimiteItensExcedido();
}
this.itens.push(new ItemPedido(produto, quantidade));
this.recalcularTotal();
}
private recalcularTotal(): void {
// Lógica complexa de cálculos de imposto, comissão, etc
}
}
// MAIS OU MENOS (Anêmico - Evite assim)
class PedidoService {
aplicarCupom(pedido: Pedido, cupom: Cupom): void {
// Toda a lógica aqui fora, pedido vira mero container de dados
if (cupom.validade < new Date()) {
throw new Error('Cupom expirado');
}
pedido.cupom = cupom;
// recalcular aqui fora...
}
}Use Value Objects para conceitos que não são apenas dados:
// Value Object
class Email {
private readonly value: string;
constructor(email: string) {
if (!this.isValid(email)) {
throw new InvalidEmailError(email);
}
this.value = email;
}
private isValid(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
getValue(): string {
return this.value;
}
equals(other: Email): boolean {
return this.value === other.value;
}
}
// Uso
class Usuario {
constructor(
public readonly email: Email, // Garantido que é válido
public readonly nome: string
) {}
}Quando algo importante acontece em um contexto, dispare eventos para que outros contextos reajam. Isso é DDD puro e MUITO útil.
// Em SalesContext (após pagamento aprovado)
class PagamentoAprovadoEvent {
constructor(
public readonly pedidoId: string,
public readonly usuarioId: string,
public readonly produtos: Array<{id: string, preco: number}>
) {}
}
// Em CatalogContext (reage ao evento)
class GerarMatriculasListener {
handle(event: PagamentoAprovadoEvent): void {
// Cria as matrículas para o usuário nos produtos comprados
// Isso desacopla completamente vendas de catálogo
}
}┌─────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ (Controllers, GraphQL Resolvers, CLI, Queue Consumers) │
├─────────────────────────────────────────────────────────────┤
│ Application Layer │
│ (Use Cases, DTOs, Command/Query Handlers) │
├─────────────────────────────────────────────────────────────┤
│ Domain Layer │
│ (Entities, Value Objects, Domain Events, Domain Services) │
├─────────────────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ (Repositories, External APIs, Database, Cache, etc) │
└─────────────────────────────────────────────────────────────┘Regras de Ouro para essa abordagem:
Vá de arquitetura pragmática com Clean Architecture como base, mas:
Essa abordagem te dá 80% dos benefícios do DDD com 20% do esforço. Você mantém a flexibilidade para pivotar rápido (essencial em startup) mas com uma base sólida que não vira "macarrão" quando o negócio crescer.
Quando (e se) o negócio explodir em complexidade, você terá a base para migrar partes específicas para DDD puro sem reescrever tudo.
Entidades anêmicas são classes que só tem dados (getters/setters) mas não tem comportamento (métodos de negócio). Todo o comportamento fica em serviços/use cases.
O verdadeiro problema das entidades anêmicas é que elas vazam responsabilidade para fora:
| Tipo | Onde usar | Exemplo |
|---|---|---|
| Rica (recomendada) | Regras de negócio complexas | Order, User, Product |
| Híbrida (aceitável) | Comportamento simples + dados | PurchaseOrder (com addItem) |
| Anêmica (com cautela) | DTOs, ORM, boundary layers | UserDTO, ProductORM |
Não é "nunca usar entidades anêmicas", é:
O anti-pattern acontece quando TODO o seu domínio é anêmico e as regras de negócio ficam espalhadas em serviços.
Nem todas as entidades precisam ser ricas - e isso é perfeitamente normal!
No seu caso, alunos podem ser entidades pobres se:
Pobre = poucas regras, mas ainda encapsula as que existem:
Só se o aluno tiver comportamentos complexos:
Conclusão: Não force complexidade onde não existe. Se aluno realmente só tem dados básicos, uma entidade pobre é a escolha correta. O importante é que as regras complexas estejam em algum lugar (serviços, value objects, outras entidades), não que todas as entidades sejam ricas.
Aluno cadastrado fazer login e assistir aula atravessa múltiplos contextos - e isso é normal!
Pergunte: "Se eu mudar X, o que mais impacta?"
Se a mudança impacta várias áreas, talvez esteja no contexto errado.
O "aluno" não existe como uma entidade única - ele é representado por diferentes conceitos em cada contexto (User, Viewer, Student). Isso é intencional e faz parte da modelagem por contextos!
// ❌ Entidade Anêmica (anti-pattern)
class User {
private name: string
private email: string
private password: string
// Só getters/setters, sem lógica de negócio
getName() { return this.name }
setName(name: string) { this.name = name }
getEmail() { return this.email }
setEmail(email: string) { this.email = email }
// ... etc
}
// Comportamento em serviços
class UserService {
changeEmail(user: User, newEmail: string) {
// Validação de email aqui
user.setEmail(newEmail)
}
}// ✅ OK: Value object anêmico (só dados)
class AddressDTO {
constructor(
public street: string,
public city: string,
public zipCode: string
) {}
}// ✅ OK: Entidade com estado, mas comportamento complexo delegado
class PurchaseOrder {
constructor(
public items: OrderItem[],
public status: OrderStatus
) {}
// Tem comportamento SIMPLES, mas o complexo fica em serviços
addItem(item: OrderItem) {
this.items.push(item)
}
// Comportamento complexo de validação de negócio
// (ex: regras de desconto, cálculo de impostos)
// fica em serviços específicos
}// ⚠️ Aceitável, mas prefira separar do domínio
@Entity()
class UserORM {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
// Sem comportamento, só mapeamento do banco
}// ❌ RUIM: Entidade anêmica + serviço com regras de domínio
class User { // Anêmica
email: string
isActive: boolean
}
class UserService {
activateUser(user: User) {
// ❌ Regra de DOMÍNIO vazando para serviço
if (!user.email.includes('@')) {
throw new Error('Invalid email')
}
user.isActive = true
}
}
// ✅ BOM: Entidade rica encapsulando regras de domínio
class User {
private constructor(
private readonly _email: string,
private _isActive: boolean
) {}
static create(email: string): User {
// Regra de domínio DENTRO da entidade
if (!email.includes('@')) {
throw new Error('Invalid email')
}
return new User(email, false)
}
activate(): void {
// Regra de domínio DENTRO da entidade
if (this._isActive) {
throw new Error('User already active')
}
this._isActive = true
}
get email() { return this._email }
get isActive() { return this._isActive }
}// Domínio: Rica em comportamento
class Product {
constructor(
private price: number,
private discount: number
) {}
calculateFinalPrice(): number {
return this.price * (1 - this.discount)
}
applyDiscount(percentage: number) {
if (percentage > 50) throw new Error('Discount too high')
this.discount = percentage
}
}
// Infra: Anêmica (só dados)
interface ProductDTO {
price: number
discount: number
}class CheckoutUseCase {
constructor(
private orderRepo: OrderRepository,
private paymentService: PaymentService
) {}
async execute(input: CheckoutInput) {
const order = await this.orderRepo.findById(input.orderId)
// Order decide se pode ser finalizada
if (!order.canBeCheckedOut()) {
throw new Error('Order cannot be checked out')
}
// Use case coordena infraestrutura
const payment = await this.paymentService.process(order.total)
// Order atualiza seu estado
order.markAsCheckedOut(payment.id)
await this.orderRepo.save(order)
}
}// ✅ PERFEITAMENTE ACEITÁVEL: Entidade de aluno pobre
class Student {
constructor(
public id: string,
public name: string,
public email: string,
public enrolledAt: Date
) {}
// Só comportamentos triviais, se houver
updateName(newName: string) {
this.name = newName
}
}
// As regras complexas vão para outros lugares:
class EnrollmentService {
canEnroll(student: Student, course: Course): boolean {
// Regras complexas de matrícula AQUI
// - Verificar se já está matriculado
// - Verificar pré-requisitos
// - Verificar limite de turma
// - Verificar período de matrícula
}
}// ❌ ANÊMICO (ruim) - dados expostos, regras vazadas
class Student {
name: string // público mutável
status: string
}
// ✅ POBRE MAS ENCAPSULADO (bom) - regras simples mas protegidas
class Student {
private _status: StudentStatus
constructor(
private _name: string,
private _email: string
) {
this._status = 'ACTIVE'
}
get name() { return this._name }
get email() { return this._email }
get status() { return this._status }
// Única regra de negócio do aluno
deactivate() {
if (this._status === 'INACTIVE') {
throw new Error('Student already inactive')
}
this._status = 'INACTIVE'
}
}// Entidade POBRE: Student (poucas regras)
class Student {
constructor(
public readonly id: string,
public name: string,
public email: string
) {}
// Regra SIMPLES: nome não pode ser vazio
changeName(newName: string) {
if (!newName.trim()) throw new Error('Name required')
this.name = newName
}
}
// Entidade RICA: Enrollment (muitas regras)
class Enrollment {
constructor(
public studentId: string,
public courseId: string,
public period: string
) {}
// Regras COMPLEXAS de matrícula
canEnroll(): boolean {
// - Verificar período de matrícula
// - Verificar vagas
// - Verificar conflito de horário
// - Verificar pré-requisitos
}
calculatePrice(): Money {
// - Calcular desconto por período
// - Aplicar bolsa do aluno
// - Calcular multa por atraso
}
validatePrerequisites(completedCourses: Course[]): boolean {
// Lógica complexa de pré-requisitos
}
}// 1. REGRAS DO ALUNO (na entidade Student)
- Nome não pode ser vazio
- Email deve ser válido
- Status pode ser ACTIVE/INACTIVE
// 2. REGRAS DE MATRÍCULA (no serviço EnrollmentService)
- Período de matrícula
- Limite de alunos por turma
- Conflito de horários
- Pré-requisitos
// 3. REGRAS DE COBRANÇA (no serviço BillingService)
- Cálculo de mensalidade
- Descontos por período
- Multas por atraso
// 4. REGRAS DE TURMA (na entidade Course)
- Capacidade máxima
- Horários disponíveis
- Professor atribuído// Se aluno tem carreira, progresso, conquistas...
class Student {
private progress: Map<CourseId, Progress>
private achievements: Achievement[]
private careerPath: CareerPath
// AÍ SIM tem regras complexas
canAdvanceToNextLevel(): boolean {
// Verificar se completou todos os cursos necessários
// Verificar tempo mínimo
// Verificar notas
}
unlockAchievement(achievement: Achievement) {
// Verificar se atende requisitos
// Calcular se é conquista especial
// Disparar eventos
}
}// PROVAVELMENTE ASSIM (pobre, mas ok):
class Student {
// Dados básicos
// Regras simples (validação de nome/email)
// Métodos básicos (ativar/desativar)
}
// REGRAS COMPLEXAS vão para:
class EnrollmentService { // Matrículas
class AttendanceService { // Frequência
class GradingService { // Notas
class PaymentService { // Pagamentos
}src/contexts/
├── iam/ # Identity and Access Management
│ ├── domain/
│ │ ├── entities/
│ │ │ └── User.ts # Credenciais, roles, permissões
│ │ ├── value-objects/
│ │ │ ├── Email.ts
│ │ │ └── Password.ts
│ │ └── services/
│ │ └── AuthenticationService.ts
│ │
│ ├── application/
│ │ ├── use-cases/
│ │ │ ├── LoginUseCase.ts
│ │ │ ├── RegisterUseCase.ts
│ │ │ └── ResetPasswordUseCase.ts
│ │ └── dtos/
│ │ └── AuthDTOs.ts
│ │
│ └── infrastructure/
│ ├── jwt/
│ └── session/src/contexts/
├── learning/ # Assistir aulas, progresso
│ ├── domain/
│ │ ├── entities/
│ │ │ ├── Enrollment.ts # Matrícula do aluno no curso
│ │ │ ├── Progress.ts # Progresso em aulas
│ │ │ └── WatchHistory.ts # Histórico de visualização
│ │ └── value-objects/
│ │ ├── CompletionStatus.ts
│ │ └── WatchTime.ts
│ │
│ ├── application/
│ │ ├── use-cases/
│ │ │ ├── WatchLessonUseCase.ts
│ │ │ ├── TrackProgressUseCase.ts
│ │ │ └── GetDashboardUseCase.ts
│ │ └── services/
│ │ └── ProgressCalculator.ts
│ │
│ └── infrastructure/
│ └── video-streaming/src/contexts/
├── catalog/ # Catálogo de cursos (leitura)
│ ├── domain/
│ │ ├── entities/
│ │ │ ├── Course.ts
│ │ │ ├── Module.ts
│ │ │ └── Lesson.ts
│ │ └── services/
│ │ └── CourseFinder.ts
│ │
│ ├── application/
│ │ └── use-cases/
│ │ ├── BrowseCoursesUseCase.ts
│ │ └── ViewLessonUseCase.ts # Visualizar CONTEÚDO da aula
│ │
│ └── infrastructure/
│ └── content-delivery/// 1. IAM: Quem é você?
class IAMContext {
async login(email: string, password: string): Promise<Session> {
// Retorna token e dados básicos do usuário
}
}
// 2. Catalog: O que você pode ver?
class CatalogContext {
async getCourse(courseId: string): Promise<Course> {
// Retorna informações do curso (independente de matrícula)
}
}
// 3. Learning: O que você já fez/acessou?
class LearningContext {
constructor(
private iam: IAMContext,
private catalog: CatalogContext
) {}
async watchLesson(userId: string, lessonId: string) {
// 1. Verifica se usuário existe (IAM)
const user = await this.iam.getUser(userId)
// 2. Verifica se aula existe (Catalog)
const lesson = await this.catalog.getLesson(lessonId)
// 3. Verifica se tem acesso (Enrollment)
if (!await this.hasAccess(userId, lesson.courseId)) {
throw new Error('Not enrolled')
}
// 4. Registra progresso
await this.trackProgress(userId, lessonId)
// 5. Retorna URL da aula (integração com infra)
return this.getStreamUrl(lesson.videoId)
}
}// IAM: Usuário com credenciais
interface User {
id: string
email: string
password: string
roles: Role[]
}
// Catalog: Visualizador (quase não existe)
interface Viewer {
id: string
preferences?: Preferences
}
// Learning: Estudante com progresso
interface Student {
userId: string
enrollments: Enrollment[]
progress: Map<CourseId, Completion>
lastAccess: Date
}// 1. IAM (NOVO) - Obrigatório
src/contexts/iam/
├── domain/
├── application/
└── infrastructure/
// 2. Catalog (já tem ideia) - Visualização
src/contexts/catalog/
├── domain/
├── application/
└── infrastructure/
// 3. Learning (NOVO) - Assistir/progresso
src/contexts/learning/
├── domain/
├── application/
└── infrastructure/// Frontend: Aluno acessa aula
async function handleWatchLesson(lessonId: string) {
// 1. Pega token do usuário logado (IAM)
const session = await iamAPI.getSession()
// 2. Busca conteúdo da aula (Catalog)
const lesson = await catalogAPI.getLesson(lessonId)
// 3. Registra acesso e pega URL do vídeo (Learning)
const { videoUrl, progress } = await learningAPI.watchLesson(
session.userId,
lessonId
)
// 4. Renderiza
return { lesson, videoUrl, progress }
}