Esse projeto foi criado com o objetivo de estudar e demonstrar diferentes padroes de design.
O Desafio do PicPay é um desafio técnico clássico de backend: construir uma API de transferências entre usuários, com regras de negócio que escondem várias decisões. Peguei para treinar Java + Spring Boot com foco em organizar o código de um jeito que eu defenderia numa code review.
A regra central: um usuário transfere dinheiro para outro. O sistema precisa checar saldo, garantir que o tipo de usuário pode enviar dinheiro, e bater num autorizador externo. Se algo falhar, nada acontece.
Coloquei a lógica de saldo dentro da entidade User, não espalhada no service. Quem sabe somar e subtrair saldo é o usuário, e ele se recusa a ficar negativo:
public void subtractBalance(BigDecimal amount) {
if (getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException(getId(), amount, getBalance());
}
setBalance(getBalance().subtract(amount));
}
Duas decisões: usar BigDecimal para dinheiro, nunca double, e deixar a entidade proteger seu próprio estado. O service não consegue zerar o saldo de alguém por engano. A entidade não deixa.
O TransactionService valida o que é dele (tipos de usuário) e delega o resto:
public Transaction saveTransaction(TransactionDTO transaction) {
if (transaction.buyer().getType() == UserType.SELLER)
throw new WrongUserTypeException(transaction.buyer().getId());
if (transaction.seller().getType() != UserType.SELLER)
throw new WrongUserTypeException(transaction.seller().getId());
if (validateTransaction()) { // bate no autorizador externo
transaction.buyer().subtractBalance(transaction.amount());
transaction.seller().addBalance(transaction.amount());
// ... monta e salva a transação
}
}
A regra "lojista não envia dinheiro, só recebe" fica explícita antes de qualquer débito.
O autorizador externo fica atrás de uma interface, não chamado direto no service:
public interface ApiGateway {
<T extends ApiDTO> T call();
}
Duas implementações. A MockyGateway faz a chamada HTTP real via RestTemplate, apontando para um endpoint do Mocky.io configurável por @Value:
@Override
public APIGatewayDTO call() {
ResponseEntity<APIGatewayDTO> response = template.getForEntity(url, APIGatewayDTO.class);
if (response == null || response.getStatusCode() != HttpStatus.OK || response.getBody() == null) {
throw new MockyDefaultExceptin("Erro na transação");
}
if (!response.getBody().message().equals("Autorizado")) {
throw new MockyAuthException("Você não está autorizado");
}
return response.getBody();
}
A FakeGateway devolve "Autorizado" sempre:
@Override
public APIGatewayDTO call() {
return new APIGatewayDTO("Autorizado");
}
Padrão Strategy/Adapter: o service depende da abstração. Em desenvolvimento injeto a FakeGateway e não dependo de internet nem de serviço de terceiro estar de pé. Trocar de provedor de autorização vira criar uma nova implementação.
Autenticação JWT: a entidade User implementa UserDetails do Spring Security, e um JwtAuthenticationFilter valida o token a cada requisição. O projeto segue camadas no estilo clean architecture: domain (entidades e regras), application (services, DTOs, gateways) e presentation (controllers).
Faltou idempotência: se o cliente reenvia a mesma requisição por timeout ou retry, dá para duplicar uma transferência. Uma chave de idempotência por requisição resolveria. E testes de integração cobrindo o fluxo completo (saldo insuficiente, lojista enviando dinheiro, autorizador recusando), que são os caminhos onde bug de dinheiro dói.