A full-stack microservices-based university management system built with Spring Boot, NestJS, React, Keycloak, RabbitMQ, and Docker.
- Architecture Overview
- Tech Stack
- Services
- Infrastructure
- Getting Started
- Keycloak Setup
- API Reference
- Feign Inter-Service Communication
- RabbitMQ Event-Driven Architecture
- Gateway Routing
- Database Schema
- Testing Guide
- Project Structure
- Troubleshooting
┌──────────────┐
│ Frontend │
│ React/Vite │
│ Port 80 │
└──────┬───────┘
│
┌──────▼───────┐
│ API Gateway │
│ Spring Cloud│
│ Port 8088 │
└──────┬───────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌─────────▼──────┐ ┌───────▼────────┐ ┌───────▼─────────┐
│ user-service │ │academic-service│ │ grade-service │
│ Port 8081 │ │ Port 8082 │ │ Port 8084 │
│ H2 (in-memory) │ │ H2 (in-memory) │ │ MySQL │
└────────────────┘ └────────────────┘ └─────────────────┘
│ │ │
┌─────────▼──────────────────▼───────────────────▼──────────┐
│ RabbitMQ (Port 5672) │
│ smart-university-exchange │
└───────────────────────────────────────────────────────────┘
│ │ │
┌─────────▼──────┐ ┌───────▼────────┐ ┌───────▼─────────┐
│ reclamation- │ │classe-niveau- │ │ Eureka Server │
│ service │ │service (NestJS)│ │ Port 8761 │
│ Port 8085 │ │ Port 3000 │ │ │
│ MySQL │ │ MongoDB │ │ │
└────────────────┘ └────────────────┘ └─────────────────┘
| Layer | Technology |
|---|---|
| Frontend | React 18, TypeScript, Vite, TailwindCSS |
| API Gateway | Spring Cloud Gateway |
| Backend (Java) | Spring Boot 3.2.x, Java 17 |
| Backend (Node) | NestJS 10, TypeScript |
| Service Discovery | Netflix Eureka |
| Authentication | Keycloak 23.0.7 (OAuth2/OIDC) |
| Messaging | RabbitMQ 3 (Topic Exchange) |
| Inter-Service | OpenFeign (Load-balanced) |
| Databases | MySQL 8.0, H2 (in-memory), MongoDB 7 |
| Containerization | Docker, Docker Compose |
| Service | Port | Tech | Database | Description |
|---|---|---|---|---|
| eureka-server | 8761 | Spring Boot | — | Service discovery |
| api-gateway | 8088 | Spring Cloud Gateway | — | Routing, JWT validation |
| user-service | 8081 | Spring Boot | H2 (in-memory) | Users, roles, Keycloak sync |
| academic-service | 8082 | Spring Boot | H2 (in-memory) | Courses, enrollments |
| grade-service | 8084 | Spring Boot | MySQL (grade_db) |
Grades, statistics |
| reclamation-service | 8085 | Spring Boot | MySQL (reclamation_db) |
Student reclamations |
| classe-niveau-service | 3000 | NestJS | MongoDB | Classes, levels (niveaux) |
| frontend | 80 | React/Vite/Nginx | — | Web UI |
| Service | Port | Credentials |
|---|---|---|
| Keycloak | 8180 | admin / admin |
| MySQL | 3306 | root / root |
| MongoDB | 27017 | — (no auth) |
| RabbitMQ | 5672 (AMQP), 15672 (UI) | guest / guest |
- Docker & Docker Compose
- PowerShell (for Keycloak setup script)
cd e:\11.04.2026
# Build all images
docker-compose build
# Start everything
docker-compose up -d
# Wait ~60 seconds for all services to be healthy
# Then setup Keycloak (see next section)Run start-all.bat — this starts each service in a separate terminal window using mvn spring-boot:run.
# Check containers
docker-compose ps
# Check Eureka (all 6 services should be UP)
# Open: http://localhost:8761After all containers are running, configure Keycloak:
# 1. Create the realm
$token = (Invoke-RestMethod -Uri "http://localhost:8180/realms/master/protocol/openid-connect/token" `
-Method Post -ContentType "application/x-www-form-urlencoded" `
-Body "username=admin&password=admin&grant_type=password&client_id=admin-cli").access_token
Invoke-RestMethod -Uri "http://localhost:8180/admin/realms" -Method Post `
-Headers @{Authorization="Bearer $token"} -ContentType "application/json" `
-Body '{"realm":"smart-university","enabled":true}'
# 2. Create client, roles, and users
powershell -ExecutionPolicy Bypass -File .\setup-keycloak.ps1| Setting | Value |
|---|---|
| Realm | smart-university |
| Client ID | gateway-client (public) |
| Roles | ADMIN, TEACHER, STUDENT |
| Username | Password | Role |
|---|---|---|
admin1 |
admin123 |
ADMIN |
teacher1 |
teacher123 |
TEACHER |
student1 |
student123 |
STUDENT |
$token = (Invoke-RestMethod -Uri "http://localhost:8180/realms/smart-university/protocol/openid-connect/token" `
-Method Post -ContentType "application/x-www-form-urlencoded" `
-Body "username=admin1&password=admin123&grant_type=password&client_id=gateway-client").access_tokenAll requests go through the API Gateway at http://localhost:8088. Include the JWT token in the Authorization: Bearer <token> header.
| Method | Endpoint | Role | Description |
|---|---|---|---|
| GET | /api/users |
ADMIN, TEACHER, STUDENT | List all users |
| GET | /api/users/{id} |
ADMIN, TEACHER | Get user by ID |
| GET | /api/users/name/{name} |
ADMIN, TEACHER, STUDENT | Get user by name |
| GET | /api/users/email/{email} |
ADMIN, TEACHER | Get user by email |
| GET | /api/users/role/{role} |
ADMIN | Get users by role |
| GET | /api/users/me |
Authenticated | Get current user |
| POST | /api/users |
ADMIN | Create user |
| PUT | /api/users/{id} |
ADMIN | Update user |
| DELETE | /api/users/{id} |
ADMIN | Delete user |
Create User:
POST /api/users
{
"name": "John Doe",
"email": "john@univ.tn",
"password": "pass123",
"role": "STUDENT"
}| Method | Endpoint | Role | Description |
|---|---|---|---|
| GET | /api/academic/courses |
ADMIN, TEACHER, STUDENT | List all courses |
| GET | /api/academic/courses/{id} |
ADMIN, TEACHER, STUDENT | Get course by ID |
| POST | /api/academic/courses |
ADMIN, TEACHER | Create course |
| PUT | /api/academic/courses/{id} |
ADMIN, TEACHER | Update course |
| DELETE | /api/academic/courses/{id} |
ADMIN | Delete course |
Create Course:
POST /api/academic/courses
{
"title": "Mathematics",
"description": "Advanced Math",
"credits": 3,
"professorId": 3
}| Method | Endpoint | Role | Description |
|---|---|---|---|
| GET | /api/academic/enrollments |
STUDENT, TEACHER, ADMIN | List all enrollments |
| GET | /api/academic/enrollments/student/{id} |
ADMIN, TEACHER, STUDENT | By student |
| GET | /api/academic/enrollments/course/{id} |
ADMIN, TEACHER | By course |
| POST | /api/academic/enrollments |
ADMIN, TEACHER | Enroll student |
| PUT | /api/academic/enrollments/{id}/cancel |
ADMIN, TEACHER | Cancel enrollment |
Create Enrollment:
POST /api/academic/enrollments
{
"studentId": 1,
"courseId": 1
}| Method | Endpoint | Role | Description |
|---|---|---|---|
| GET | /api/grades |
STUDENT, TEACHER, ADMIN | List all grades |
| GET | /api/grades/{id} |
STUDENT, TEACHER, ADMIN | Get grade by ID |
| GET | /api/grades/{id}/details |
TEACHER, ADMIN | Grade + student info (Feign) |
| POST | /api/grades |
TEACHER, ADMIN | Create grade |
| PUT | /api/grades/{id} |
TEACHER, ADMIN | Update grade |
| DELETE | /api/grades/{id} |
ADMIN | Delete grade |
| GET | /api/grades/stats/average |
Public | Average score |
| GET | /api/grades/stats/max |
Public | Max score |
| GET | /api/grades/stats/min |
Public | Min score |
| GET | /api/grades/stats/count |
Public | Total count |
Create Grade:
POST /api/grades
{
"studentName": "Ahmed Zayen",
"subject": "Mathematics",
"examType": "Final",
"semester": "Spring 2026",
"score": 85.5
}| Method | Endpoint | Role | Description |
|---|---|---|---|
| GET | /api/reclamations |
TEACHER, ADMIN | List all reclamations |
| GET | /api/reclamations/student/{username} |
STUDENT, TEACHER, ADMIN | By student |
| POST | /api/reclamations |
STUDENT | Create reclamation |
| PUT | /api/reclamations/{id} |
TEACHER, ADMIN | Update reclamation |
| DELETE | /api/reclamations/{id} |
ADMIN | Delete reclamation |
Create Reclamation:
POST /api/reclamations
{
"studentName": "Ahmed Zayen",
"subject": "Mathematics",
"description": "Grade seems incorrect",
"type": "NOTE"
}Types:
NOTE(validates course via Feign),GENERAL(no course validation)
| Method | Endpoint | Role | Description |
|---|---|---|---|
| GET | /api/classes |
STUDENT, TEACHER, ADMIN | List all classes |
| GET | /api/classes/{id} |
STUDENT, TEACHER, ADMIN | Get class by ID |
| GET | /api/classes/{id}/etudiants |
TEACHER, ADMIN | Get students in class |
| POST | /api/classes |
ADMIN | Create class |
| PATCH | /api/classes/{id} |
ADMIN | Update class |
| DELETE | /api/classes/{id} |
ADMIN | Delete class |
| POST | /api/classes/{id}/etudiants |
ADMIN | Add students to class |
| Method | Endpoint | Role | Description |
|---|---|---|---|
| GET | /api/niveaux |
STUDENT, TEACHER, ADMIN | List all levels |
| GET | /api/niveaux/{id} |
STUDENT, TEACHER, ADMIN | Get level by ID |
| POST | /api/niveaux |
ADMIN | Create level |
| PATCH | /api/niveaux/{id} |
ADMIN | Update level |
| DELETE | /api/niveaux/{id} |
ADMIN | Delete level |
Services communicate synchronously using OpenFeign with Eureka load-balancing. A FeignConfig bean relays the JWT token from incoming requests to outgoing Feign calls.
┌─────────────────────┐ Feign ┌─────────────────┐
│ reclamation-service │──────────────────►│ user-service │
│ │ getUserByName() │ │
│ │──────────────────►│ academic-service│
│ │ getAllCourses() │ │
└──────────────────────┘ └──────────────────┘
┌─────────────────────┐ Feign ┌─────────────────┐
│ grade-service │──────────────────►│ user-service │
│ │ getUserByName() │ │
│ │──────────────────►│ academic-service│
│ │ getAllCourses() │ │
└──────────────────────┘ └──────────────────┘
┌─────────────────────┐ Feign ┌─────────────────┐
│ academic-service │──────────────────►│ user-service │
│ │ getUserById() │ │
│ │──────────────────►│ grade-service │
│ │ getAllGrades() │ │
└──────────────────────┘ └──────────────────┘
┌─────────────────────┐ Feign ┌─────────────────┐
│ user-service │──────────────────►│ academic-service│
│ │ getAllCourses() │ │
└──────────────────────┘ └──────────────────┘
| Service | Action | Feign Calls |
|---|---|---|
| reclamation-service | Create reclamation | user-service.getUserByName() — validate student exists |
| reclamation-service | Create (type=NOTE) | academic-service.getAllCourses() — validate course exists |
| grade-service | Create grade | user-service.getUserByName() — validate student |
| grade-service | Create grade | academic-service.getAllCourses() — validate course |
| grade-service | GET /grades/{id}/details |
user-service.getUserByName() — enrich with email/role |
| academic-service | Create enrollment | user-service.getUserById() — validate student exists |
All services connect to a shared Topic Exchange (smart-university-exchange). Events are routed using topic patterns.
| Queue | Service | Binding Pattern | Purpose |
|---|---|---|---|
enrollment.queue |
academic-service | enrollment.* |
Own enrollment events |
course.queue |
academic-service | course.* |
Own course events |
academic.grade.queue |
academic-service | grade.* |
Receive grade notifications |
academic.reclamation.queue |
academic-service | reclamation.* |
Receive reclamation notifications |
user.notification.queue |
user-service | enrollment.*, grade.*, reclamation.* |
Notification hub (all events) |
grade.queue |
grade-service | grade.* |
Own grade events |
grade.enrollment.queue |
grade-service | enrollment.* |
React to student enrollments |
grade.course.queue |
grade-service | course.* |
React to course changes |
reclamation.queue |
reclamation-service | reclamation.* |
Own reclamation events |
reclamation.grade.queue |
reclamation-service | grade.* |
React to grade changes |
Action Producer Routing Key Consumers
─────────────────────────────────────────────────────────────────────────────────
Create enrollment → academic-service → enrollment.created → user-service
grade-service
Cancel enrollment → academic-service → enrollment.cancelled → user-service
grade-service
Create course → academic-service → course.created → grade-service
Update course → academic-service → course.updated → grade-service
Delete course → academic-service → course.deleted → grade-service
Create grade → grade-service → grade.created → user-service
academic-service
reclamation-service
Update grade → grade-service → grade.updated → user-service
academic-service
reclamation-service
Create reclamation → reclamation-svc → reclamation.created → user-service
academic-service
Update reclamation → reclamation-svc → reclamation.updated → user-service
academic-service
EnrollmentEvent:
{
"enrollmentId": 1,
"studentId": 1,
"courseId": 1,
"status": "PENDING",
"eventType": "CREATED",
"timestamp": "2026-04-13T20:01:29"
}GradeEvent:
{
"gradeId": 1,
"studentName": "Ahmed Zayen",
"subject": "Mathematics",
"score": 85.5,
"eventType": "CREATED",
"timestamp": "2026-04-13T20:09:33"
}ReclamationEvent:
{
"reclamationId": 1,
"studentName": "Ahmed Zayen",
"subject": "Mathematics",
"type": "NOTE",
"status": "PENDING",
"eventType": "CREATED",
"timestamp": "2026-04-13T20:08:17"
}CourseEvent:
{
"courseId": 1,
"courseName": "Mathematics",
"eventType": "CREATED",
"timestamp": "2026-04-13T20:07:58"
}The API Gateway (port 8088) routes requests and rewrites paths:
| Gateway Path | Target Service | Internal Path | Rewrite |
|---|---|---|---|
/api/users/** |
user-service | /api/users/** |
None (pass-through) |
/api/academic/** |
academic-service | /api/** |
/api/academic/X → /api/X |
/api/grades/** |
grade-service | /grades/** |
/api/grades/X → /grades/X |
/api/reclamations/** |
reclamation-service | /reclamations/** |
/api/reclamations/X → /reclamations/X |
/api/classes/** |
classe-niveau-service | /classes/** |
/api/classes/X → /classes/X |
/api/niveaux/** |
classe-niveau-service | /niveaux/** |
/api/niveaux/X → /niveaux/X |
users
| Column | Type | Constraints |
|---|---|---|
| id | BIGINT | PK, auto-increment |
| name | VARCHAR | NOT NULL |
| VARCHAR | NOT NULL, UNIQUE | |
| password | VARCHAR | NOT NULL |
| role | VARCHAR | NOT NULL (STUDENT, TEACHER, ADMIN) |
5 users seeded on startup: Ahmed Zayen, Marwa Derbel, Sarah Mohsen, Karim Ben Ali, Admin Omar
courses
| Column | Type | Constraints |
|---|---|---|
| id | BIGINT | PK, auto-increment |
| title | VARCHAR(100) | NOT NULL, UNIQUE |
| description | TEXT | |
| credits | INT | NOT NULL |
| professor_id | BIGINT | |
| created_at | TIMESTAMP | Auto |
enrollments
| Column | Type | Constraints |
|---|---|---|
| id | BIGINT | PK, auto-increment |
| student_id | BIGINT | NOT NULL |
| course_id | BIGINT | FK → courses, NOT NULL |
| enrollment_date | DATE | Auto (current date) |
| status | VARCHAR | PENDING, CONFIRMED, CANCELLED |
grade
| Column | Type | Constraints |
|---|---|---|
| id | BIGINT | PK, auto-increment |
| student_name | VARCHAR | |
| subject | VARCHAR | |
| exam_type | VARCHAR | |
| semester | VARCHAR | |
| score | DOUBLE | |
| created_at | TIMESTAMP | Auto |
reclamation
| Column | Type | Constraints |
|---|---|---|
| id | BIGINT | PK, auto-increment |
| student_name | VARCHAR | |
| subject | VARCHAR | |
| description | VARCHAR(2000) | |
| type | VARCHAR | NOTE, GENERAL |
| status | VARCHAR | PENDING, RESOLVED, ... |
| created_at | TIMESTAMP | |
| updated_at | TIMESTAMP |
classes and niveaux collections (schema-less, managed by Mongoose).
$adminToken = (Invoke-RestMethod -Uri "http://localhost:8180/realms/smart-university/protocol/openid-connect/token" -Method Post -ContentType "application/x-www-form-urlencoded" -Body "username=admin1&password=admin123&grant_type=password&client_id=gateway-client").access_token
$teacherToken = (Invoke-RestMethod -Uri "http://localhost:8180/realms/smart-university/protocol/openid-connect/token" -Method Post -ContentType "application/x-www-form-urlencoded" -Body "username=teacher1&password=teacher123&grant_type=password&client_id=gateway-client").access_token
$studentToken = (Invoke-RestMethod -Uri "http://localhost:8180/realms/smart-university/protocol/openid-connect/token" -Method Post -ContentType "application/x-www-form-urlencoded" -Body "username=student1&password=student123&grant_type=password&client_id=gateway-client").access_token# List seeded users
Invoke-RestMethod -Uri "http://localhost:8088/api/users" -Headers @{Authorization="Bearer $adminToken"}
# Create a course
Invoke-RestMethod -Uri "http://localhost:8088/api/academic/courses" -Method Post -Headers @{Authorization="Bearer $adminToken"} -ContentType "application/json" -Body '{"title":"Mathematics","description":"Advanced Math","credits":3,"professorId":3}'# type=NOTE → Feign validates student + course
Invoke-RestMethod -Uri "http://localhost:8088/api/reclamations" -Method Post -Headers @{Authorization="Bearer $studentToken"} -ContentType "application/json" -Body '{"studentName":"Ahmed Zayen","subject":"Mathematics","description":"Grade incorrect","type":"NOTE"}'
# Verify Feign logs
docker logs reclamation-service 2>&1 | Select-String "Feign"# Create grade
Invoke-RestMethod -Uri "http://localhost:8088/api/grades" -Method Post -Headers @{Authorization="Bearer $teacherToken"} -ContentType "application/json" -Body '{"studentName":"Ahmed Zayen","subject":"Mathematics","examType":"Final","semester":"Spring 2026","score":85.5}'
# Get with Feign enrichment (email, role from user-service)
Invoke-RestMethod -Uri "http://localhost:8088/api/grades/1/details" -Headers @{Authorization="Bearer $teacherToken"}
# Verify Feign logs
docker logs grade-service 2>&1 | Select-String "Feign"# Enroll student (Feign validates via user-service)
Invoke-RestMethod -Uri "http://localhost:8088/api/academic/enrollments" -Method Post -Headers @{Authorization="Bearer $adminToken"} -ContentType "application/json" -Body '{"studentId":1,"courseId":1}'
# Cancel
Invoke-RestMethod -Uri "http://localhost:8088/api/academic/enrollments/1/cancel" -Method Put -Headers @{Authorization="Bearer $adminToken"}
# Verify Feign logs
docker logs academic-service 2>&1 | Select-String "Feign"# Check events received by each service
docker logs user-service 2>&1 | Select-String "RabbitMQ"
docker logs academic-service 2>&1 | Select-String "RabbitMQ"
docker logs grade-service 2>&1 | Select-String "RabbitMQ"
docker logs reclamation-service 2>&1 | Select-String "RabbitMQ"Or open http://localhost:15672 (guest/guest) → Queues tab.
smart-university/
├── docker-compose.yml # Full stack orchestration
├── init-db.sql # Creates grade_db and reclamation_db
├── setup-keycloak.ps1 # Keycloak realm/roles/users setup
├── start-all.bat # Start all services locally
├── stop-all.bat # Stop all services
├── README.md
│
├── eureka-server/ # Service Discovery (Spring Boot)
│ └── src/main/
│
├── api-gateway/ # API Gateway (Spring Cloud Gateway)
│ └── src/main/resources/
│ ├── application.yml
│ └── application-docker.yml
│
├── user-service/ # User Management (Spring Boot + H2)
│ └── src/main/java/esprit/userservice/
│ ├── controller/UserRestAPI.java
│ ├── model/User.java
│ ├── client/AcademicServiceClient.java (Feign)
│ ├── config/
│ │ ├── SecurityConfig.java
│ │ ├── RabbitMQConfig.java
│ │ ├── FeignConfig.java
│ │ └── DataInitializer.java
│ └── event/UserEventListener.java
│
├── academic-service/ # Courses & Enrollments (Spring Boot + H2)
│ └── src/main/java/com/univ/academic/
│ ├── controller/
│ │ ├── CourseController.java
│ │ └── EnrollmentController.java
│ ├── entity/Course.java, Enrollment.java
│ ├── dto/
│ ├── client/
│ │ ├── UserServiceClient.java (Feign)
│ │ └── GradeServiceClient.java (Feign)
│ ├── config/
│ │ ├── SecurityConfig.java
│ │ ├── RabbitMQConfig.java
│ │ └── FeignConfig.java
│ └── event/
│
├── Grade-Service/Backend/Grade-Service/ # Grades (Spring Boot + MySQL)
│ └── src/main/java/org/example/gradeservice/
│ ├── GradeRestAPI.java
│ ├── Grade.java
│ ├── GradeService.java
│ ├── client/
│ │ ├── UserServiceClient.java (Feign)
│ │ └── AcademicServiceClient.java (Feign)
│ ├── config/
│ │ ├── SecurityConfig.java
│ │ ├── RabbitMQConfig.java
│ │ └── FeignConfig.java
│ └── event/
│
├── reclamation_service/ # Reclamations (Spring Boot + MySQL)
│ └── src/main/java/org/example/reclamationservice/
│ ├── controller/ReclamationController.java
│ ├── entity/Reclamation.java
│ ├── service/ReclamationService.java
│ ├── client/
│ │ ├── UserServiceClient.java (Feign)
│ │ └── AcademicServiceClient.java (Feign)
│ ├── config/
│ │ ├── SecurityConfig.java
│ │ ├── RabbitMQConfig.java
│ │ └── FeignConfig.java
│ └── event/
│
├── classe-niveau-service/ # Classes & Levels (NestJS + MongoDB)
│ └── src/
│ ├── app.module.ts
│ ├── main.ts
│ ├── auth/ # JWT guards
│ ├── classes/ # Classes CRUD
│ ├── niveaux/ # Niveaux CRUD
│ └── eureka/ # Eureka registration
│
└── smart-university-frontend/ # React Frontend
└── src/
├── App.tsx
├── api/ # Axios clients
├── auth/ # Keycloak integration
├── components/ # UI components
├── pages/ # Route pages
└── types/ # TypeScript types
| Problem | Solution |
|---|---|
Container shows Exited |
Run docker logs <name> to see error |
401 Unauthorized |
Token expired — get a new one |
Realm not found |
Create realm first (see Keycloak Setup) |
User already exists in Keycloak |
User was already created — use a different name |
| Data missing after restart | user-service & academic-service use H2 in-memory — data resets |
| Feign 401/403 | FeignConfig must relay JWT — check FeignConfig.java exists |
| Service not in Eureka | Wait 30s — services register after startup |
| RabbitMQ connection refused | Wait for RabbitMQ healthcheck to pass |
| Port already in use | Run docker-compose down then docker-compose up -d |
| Frontend can't reach API | Check VITE_API_GATEWAY_URL in docker-compose.yml |
# View logs for a specific service
docker logs <service-name> -f
# Restart a single service
docker-compose restart <service-name>
# Rebuild and restart one service
docker-compose build <service-name> && docker-compose up -d <service-name>
# Stop everything
docker-compose down
# Stop and remove volumes (full reset)
docker-compose down -v| Resource | URL |
|---|---|
| Frontend | http://localhost |
| API Gateway | http://localhost:8088 |
| Eureka Dashboard | http://localhost:8761 |
| Keycloak Admin | http://localhost:8180 |
| RabbitMQ Dashboard | http://localhost:15672 |
| H2 Console (user-service) | http://localhost:8081/h2-console |
| H2 Console (academic-service) | http://localhost:8082/h2-console |