
Se você já domina bem o Java, entende de Spring Boot e já ouviu falar de Docker, Kubernetes e filas como RabbitMQ, mas ainda sente falta de projetos que realmente conectem tudo isso, esse Borapraticar é pra você!
Hoje vamos criar juntos uma plataforma de reservas de salas de reunião. O foco não é só codar, mas também exercitar aquela habilidade que todo pleno precisa começar a treinar: system design.
🎯 O que vamos construir?
A ideia é simples:
- Um usuário faz login na plataforma.
- Ele pode reservar ou cancelar uma sala de reunião.
- Quando isso acontece, outro serviço fica sabendo e dispara notificações (log, e-mail fake, ou o que você quiser).
🧩 Arquitetura do projeto
Nada melhor do que um desenho pra explicar:
+---------------------+
| API Gateway |
+---------------------+
|
+------------------+-------------------+
| |
+-----------+ +-----------+
| Auth Svc | | Booking Svc|
| (JWT) | | (Reservas) |
+-----------+ +-----------+
|
v
+-----------+
| RabbitMQ |
+-----------+
|
+-----------+
| Notif Svc |
| (Logs) |
+-----------+
Componentes:
- API Gateway → entrada única para os serviços.
- Booking auth → autenticação e emissão de tokens JWT.
- Booking Service → CRUD de reservas, publica eventos no RabbitMQ.
- Notification Service → consome eventos do RabbitMQ e gera notificações.
- PostgreSQL → banco de dados das reservas.
🔧 Tecnologias utilizadas
- Java 21 + Spring Boot 6
- RabbitMQ
- PostgreSQL
- Docker & Kubernetes (Kind ou K3d)
- Spring Actuator para métricas básicas
💻 Mão na massa
1. Criando os serviços
Cada serviço será um projeto Spring Boot separado.
👉 Booking auth :
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- JWT para autenticação -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Login fake
import com.setedevs.auth.model.LoginRequest;
import com.setedevs.auth.utils.JwtUtil;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody LoginRequest request) {
// valida usuário (fake: admin / 123)
if ("admin".equals(request.getUsername()) && "123".equals(request.getPassword())) {
String token = JwtUtil.generateToken(request.getUsername());
return ResponseEntity.ok(token);
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Credenciais inválidas");
}
}
Utils
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil {
private static final String SECRET_KEY = "404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970";
private static final SecretKey key = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
// Tempo de expiração do token (24 horas)
private static final long EXPIRATION_TIME = 24 * 60 * 60 * 1000;
public static String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, username);
}
private static String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
public Boolean validateToken(String token, String username) {
final String tokenUsername = extractUsername(token);
return (username.equals(tokenUsername) && !isTokenExpired(token));
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
}
👉 Booking Service
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Exemplo de endpoint no Booking Service:
@PostMapping("/bookings")
public ResponseEntity createBooking(@RequestBody BookingRequest request) {
bookingService.createBooking(request);
return ResponseEntity.ok("Reserva criada com sucesso!");
}
Esse bookingService.createBooking além de salvar no banco, envia uma mensagem pro RabbitMQ com os detalhes da reserva.
E o BookingService publica no RabbitMQ:
@Service
public class BookingService {
@Autowired
private BookingRepository bookingRepository;
@Autowired
private RabbitTemplate rabbitTemplate;
public void createBooking(BookingRequest request) {
bookingRepository.save(request.toEntity());
rabbitTemplate.convertAndSend("bookings.exchange", "booking.created", request);
}
}
👉 Notification Service
pom.xml
<dependencies>
<!-- Web (para expor health check e actuator) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- RabbitMQ para consumir mensagens -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>
Consumindo as notificações
@Service
public class NotificationConsumer {
@RabbitListener(queues = "booking.notifications")
public void receiveMessage(BookingRequest request) {
System.out.println("📢 Nova reserva recebida: " + request);
}
}
👉 API Gateway com Spring Cloud Gateway (em Java) 🚪
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
Configuração (application.yml)
spring:
cloud:
gateway:
routes:
- id: auth-service
uri: http://auth-service:8080
predicates:
- Path=/auth/**
- id: booking-service
uri: http://booking-service:8080
predicates:
- Path=/bookings/**
- id: notification-service
uri: http://notification-service:8080
predicates:
- Path=/notifications/**
Agora, o usuário acessa:
http://localhost:8080/auth/loginhttp://localhost:8080/bookingshttp://localhost:8080/notifications
Git dos projetos:
Gateway: https://github.com/devsdofuturobr/booking-gateway.git
Auth: https://github.com/devsdofuturobr/booking-auth.git
Booking: https://github.com/devsdofuturobr/booking
Notification: https://github.com/devsdofuturobr/booking-notification.git
5. Deploy com Docker + Kubernetes
Cada serviço terá seu Dockerfile, e depois a mágica acontece no Kubernetes:
📦 Dockerfile base (para todos os serviços)
FROM eclipse-temurin:21-jdk-alpine as build
WORKDIR /app
COPY . .
RUN ./mvnw clean package -DskipTests
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
🧠 Exercício de System Design
A graça desse projeto é você não só rodar, mas também pensar em como evoluir:
- Escalabilidade → e se 100 pessoas tentarem reservar ao mesmo tempo?
- Resiliência → o que acontece se o Notification Service cair?
- Observabilidade → como monitorar as reservas por hora?
Esses pontos são o que diferenciam um dev pleno de alguém que já está virando senior.
🔥 Próximos Passos
- Adicionar Prometheus + Grafana pra monitorar o cluster.
- Testar com Kafka no lugar do RabbitMQ.
- Implementar Dead Letter Queue para mensagens que falharem.
🎬 Conclusão
Esse projetinho é simples, mas poderoso. Ele conecta Java, mensageria, banco de dados e Kubernetes em um fluxo real, que você pode evoluir infinitamente.
E se você quiser aprender na prática, com vídeo passo a passo, recomendo visitar meu canal BoraPraticar no YouTube. Lá tem muito conteúdo que vai complementar o que vimos aqui.
Até o próximo artigo, devs! 🚀