Skip to content

vinicius1209/subscription-play

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SubscriptionPlay - Sistema de Gestão de Assinaturas

Sistema de gestão de assinaturas para serviço de streaming. Implementa cadastro de usuários, criação de assinaturas, renovação automática e cancelamento.

Tecnologias Utilizadas

  • Java 17 + Spring Boot 3.2
  • PostgreSQL 15 - Persistência de dados
  • Redis 7 - Cache de assinaturas ativas
  • RabbitMQ 3 - Mensageria para processamento assíncrono de renovações
  • Flyway - Versionamento de schema do banco de dados
  • JUnit 5 + Mockito + Testcontainers - Testes automatizados
  • SpringDoc OpenAPI - Documentação da API (Swagger)

Arquitetura

O projeto segue os princípios da Clean Architecture combinados com Domain-Driven Design (DDD), organizando o código em camadas com responsabilidades bem definidas:

src/main/java/br/com/subscriptionplay/
├── application/          
│   ├── controller/       # Controllers REST
│   ├── request/          # DTOs de entrada
│   └── response/         # DTOs de saída
├── domain/               
│   ├── model/            # Entidades e objetos de valor
│   ├── gateway/          # Interfaces (ports)
│   ├── exception/        # Exceções de domínio
│   └── usecase/          # Casos de uso
└── infra/                
    ├── config/           # Configurações do Spring
    ├── persistence/      # JPA e repositórios
    ├── cache/            # Implementação Redis
    ├── messaging/        # Implementação RabbitMQ
    └── jobs/             # Tarefas

Por que essa estrutura?

A separação em camadas garante que o domínio permanece isolado de frameworks e bibliotecas externas. Os casos de uso dependem apenas de interfaces (gateways), e as implementações concretas são injetadas pelo Spring. Isso facilita:

  • Testabilidade: Os casos de uso podem ser testados com mocks, sem necessidade de banco de dados ou mensageria.
  • Flexibilidade: Trocar o PostgreSQL por outro banco, ou RabbitMQ por Kafka, requer apenas nova implementação do gateway.
  • Manutenibilidade: Regras de negócio centralizadas no domínio, sem espalhamento por controllers ou repositórios.

Decisões Arquiteturais

1. Tratamento de Concorrência na renovação de assinaturas

O sistema utiliza lock pessimista (SELECT FOR UPDATE) para evitar que a mesma assinatura seja renovada simultaneamente por múltiplas instâncias da aplicação.

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT s FROM SubscriptionEntity s WHERE s.id = :id")
Optional<SubscriptionEntity> findByIdForUpdate(UUID id);

Trade-off considerado: Em um cenário real com gateway de pagamento externo, manter o lock durante uma chamada HTTP seria problemático (a linha ficaria travada por segundos). A solução ideal seria:

  1. Adquirir lock e validar elegibilidade
  2. Liberar lock e chamar gateway de pagamento
  3. Adquirir novo lock e atualizar resultado

Para este desafio, como o PaymentGateway é um mock síncrono, o lock durante toda a operação é aceitável.

2. Idempotência no processamento de mensagens

O RabbitMQ garante entrega "at least once", o que significa que uma mensagem pode ser entregue mais de uma vez. Para evitar renovações duplicadas, o sistema verifica se a assinatura já foi renovada:

if (subscription.wasAlreadyRenewedToday()) {
    return RenewalResult.skipped("Subscription already renewed");
}

3. Rastreabilidade

Cada execução do job de renovação gera um identificador único que é propagado através das mensagens e logs:

[JOB-a1b2c3d4] Starting subscription renewal job
[a1b2c3d4-e5f6] Starting renewal for subscription: xxx-yyy
[a1b2c3d4-e5f6] Renewal completed successfully for subscription xxx-yyy

Isso permite rastrear o ciclo de vida completo de uma renovação em ambientes distribuídos.

4. Cache

As consultas de assinaturas ativas utilizam Redis como cache, seguindo o padrão cache-aside:

  1. Verifica se existe no cache
  2. Se não existir, busca no banco e armazena no cache
  3. Operações de escrita (cancelamento, suspensão) invalidam o cache

O TTL de 1 hora garante que dados obsoletos não permaneçam indefinidamente.

5. Processamento assíncrono de renovações

O job diário não processa as renovações diretamente. Ele publica mensagens no RabbitMQ, que são consumidas assincronamente:

Job (6h) → Publica N mensagens → RabbitMQ → Consumer processa uma a uma

Vantagens:

  • O job termina rapidamente, independente do volume
  • Falhas são isoladas por assinatura
  • Mensagens com erro vão para DLQ para análise posterior
  • Possibilidade de escalar consumidores horizontalmente

6. Gateway como porta de saída

As interfaces no pacote domain/gateway definem contratos que o domínio espera, sem conhecer as implementações:

Interface Implementação Responsabilidade
SubscriptionGateway SubscriptionGatewayImpl Persistência JPA
PaymentGateway PaymentGatewayMock Simulação de pagamento
SubscriptionCacheGateway SubscriptionCacheGatewayImpl Cache Redis
SubscriptionEventPublisher SubscriptionEventPublisherImpl Mensageria RabbitMQ

Trade-offs e decisões de modelagem

1. VARCHAR ao invés de ENUM no PostgreSQL

Os campos status e plan são armazenados como VARCHAR no banco, não como tipo ENUM nativo do PostgreSQL.

Motivo: ENUMs no PostgreSQL são difíceis de modificar. Adicionar um valor é simples (ALTER TYPE ... ADD VALUE), mas remover ou renomear exige criar um novo tipo, migrar os dados e dropar o antigo.

Com VARCHAR, a validação acontece na camada de domínio através dos enums Java (Plan, SubscriptionStatus). O banco permanece flexível para evoluções, e as regras de negócio ficam onde devem estar: no domínio.

Trade-off aceito: Perdemos validação em nível de banco em troca de migrations mais simples.

2. Planos como Enum ao invés de Tabela

Os planos (BASICO, PREMIUM, FAMILIA) estão definidos como enum Java, não como uma tabela plans no banco.

Motivo: O requisito especifica exatamente três planos com preços fixos. Criar uma tabela dinâmica adicionaria:

  • Novas entidades e repositórios
  • CRUD de planos com endpoints
  • Validações de FK ao criar assinatura
  • Complexidade sem necessidade imediata

Princípio aplicado: YAGNI (You Aren't Gonna Need It). A solução atende ao requisito sem over-engineering.

Evolução futura: Se surgir necessidade de planos dinâmicos (promoções, regionalização), a arquitetura permite extrair para uma tabela sem reescrever o domínio. O CreateSubscriptionUseCase passaria a buscar o plano via gateway ao invés de receber diretamente o enum.

3. Testcontainers para testes integrados

Os testes de integração utilizam Testcontainers ao invés de banco H2 em memória ou infraestrutura compartilhada.

Motivo:

  • Fidelidade: Testes rodam contra PostgreSQL e Redis reais, não simulações
  • Isolamento: Cada execução tem ambiente limpo, sem interferência entre testes
  • Simplicidade: mvn test funciona em qualquer máquina com Docker, sem configuração prévia

Trade-off aceito: Testes de integração são mais lentos (containers precisam subir), mas a confiabilidade compensa. Testes unitários continuam rápidos usando mocks.

5. Idioma Inglês Padronizado

Logs, exceções e mensagens de erro estão em inglês.

Motivo:

  • Padrão da indústria
  • Facilita busca por erros em Stack Overflow e documentações
  • Compatível com equipes internacionais ou distribuídas (Principalmente consultorias)
  • Mensagens de API seguem convenções REST globais

Exceções em português: Apenas os nomes dos planos (BASICO, PREMIUM, FAMILIA) e status (ATIVA, SUSPENSA, CANCELADA) permanecem em português pois são parte do domínio de negócio brasileiro e aparecem nos dados do banco.

6. Snapshot de Preço (não implementado)

A assinatura não armazena o preço pago no momento da contratação. Se o preço do plano mudar, não há histórico.

Motivo: Não estava no requisito. Em um sistema real, seria interessante adicionar um campo priceAtCreation na assinatura para auditoria e disputas de cobrança.

Evolução futura: Adicionar coluna price_at_creation na migration e capturar o valor no CreateSubscriptionUseCase.

Executando o Projeto

Pré-requisitos

  • Java 17+
  • Maven 3.8+
  • Docker e Docker Compose

1. Subir a infraestrutura

docker-compose up -d

Isso iniciará:

  • PostgreSQL na porta 5433 (5432 geralmente tem outros projetos :P)
  • Redis na porta 6379
  • RabbitMQ na porta 5672 (management UI na 15672)

2. Executar a aplicação

mvn spring-boot:run

A aplicação estará disponível em http://localhost:8080.

3. Acessar a documentação da API

http://localhost:8080/swagger-ui.html

4. Executar os testes

mvn test

Os testes de integração utilizam Testcontainers, que sobem containers Docker automaticamente.

Endpoints da API

Método Endpoint Descrição
POST /api/users Criar usuário
POST /api/subscriptions Criar assinatura
GET /api/subscriptions/user/{userId}/active Buscar assinatura ativa
POST /api/subscriptions/{id}/cancel Cancelar assinatura

Exemplos de requests

Criar usuário:

curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"email": "usuario@email.com", "name": "João Silva"}'

Criar assinatura:

curl -X POST http://localhost:8080/api/subscriptions \
  -H "Content-Type: application/json" \
  -d '{"userId": "uuid-do-usuario", "plan": "PREMIUM"}'

Planos disponíveis: BASICO (R$ 19,90), PREMIUM (R$ 39,90), FAMILIA (R$ 59,90)

Regras de Negócio

  1. Um usuário pode ter apenas uma assinatura ativa por vez
  2. Renovação automática: Executada diariamente às 6h para assinaturas vencendo no dia
  3. Suspensão após falhas: Três tentativas de pagamento falhadas suspendem a assinatura
  4. Cancelamento preserva acesso: Usuário continua utilizando o serviço até a data de expiração original

Estrutura de testes

src/test/java/
├── domain/
│   ├── model/
│   │   └── SubscriptionTest.java                    # Testes da entidade de domínio
│   └── usecase/
│       ├── CancelSubscriptionUseCaseTest.java       # Testes de cancelamento
│       ├── CreateSubscriptionUseCaseTest.java       # Testes de criação
│       ├── GetActiveSubscriptionUseCaseTest.java    # Testes de busca com cache
│       └── ProcessSubscriptionRenewalUseCaseTest.java  # Testes de renovação
└── SubscriptionIntegrationTest.java                 # Testes de integração com Testcontainers

Cobertura dos testes (36 testes):

  • Criação de assinatura com validações
  • Renovação bem-sucedida e falhas de pagamento
  • Suspensão após 3 falhas consecutivas
  • Idempotência na renovação
  • Cancelamento mantendo data de expiração
  • Cache-aside (hit/miss/invalidação)
  • Validações de domínio (status, planos)

Melhorias futuras

Funcionalidades que poderiam ser adicionadas em uma evolução do sistema:

  • Retry com backoff exponencial para falhas de pagamento
  • Notificações por email/push quando a assinatura for suspensa
  • Métricas e observabilidade com Micrometer e Prometheus
  • Histórico de transações para auditoria

About

Subscription api template

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors