A production-grade LMS backend microservice built with Spring Boot. It models the full lifecycle of online courses — from catalog management and student enrollment to progress tracking, automated certification, and peer reviews — exposing all workflows via clean REST endpoints backed by a resilient, event-driven architecture.
Enrolling a student in a course sounds simple. In reality it spans user verification, payment processing, idempotency, fault tolerance, and downstream notifications. This service handles all of it while staying decoupled from its collaborators.
- What It Does
- Architecture Overview
- API Reference
- Enrollment Flow
- LMS Features — Progress, Certificates, Ratings
- Event-Driven Design
- Cross-Repo Integration (CNKart → EdTech)
- Observability
- Security
- Data Model
- Performance Baseline
- Running Locally
- Testing
- Tech Stack
| Capability | Description |
|---|---|
| 📚 Course Catalog | Create, update, delete, and search courses by id, name, or instructor |
| 🎓 Student Enrollment | Full lifecycle state machine with idempotent duplicate handling |
| ✅ Progress Tracking | Students mark materials complete; progress is persisted per user |
| 🏆 Certificate Issuance | Auto-published via RabbitMQ when all materials are completed |
| ⭐ Ratings & Reviews | Only enrolled students can rate; average rating served on every course response |
| 📡 Event Publishing | RabbitMQ events for every enrollment lifecycle transition |
| 🔗 Cross-Repo Trigger | Listens for CNKart order.confirmed → auto-enrolls buyer in a course bundle |
| 🔒 Security | OAuth2 Resource Server with JWT, SecurityAuditFilter logging every access |
| 🔍 Observability | Spring Cloud Sleuth + Zipkin tracing, Micrometer + Prometheus metrics |
| 🧪 Testcontainers | Integration tests spin up real MySQL + RabbitMQ containers — no H2 mocks |
| ⚡ Resilience | Resilience4j circuit-breaker + retry on all external service calls |
graph TD
subgraph Clients
A[REST Client]
B[CNKart Order Service]
end
subgraph EdTech Course Service
C[CourseController]
D[CourseProgressController]
E[CourseService]
F[CourseProgressService]
G[CourseEventPublisher]
H[OrderEventListener]
I[SecurityAuditFilter]
J[SecurityConfig / OAuth2]
end
subgraph Infrastructure
K[(MySQL)]
L[(RabbitMQ)]
M[Zipkin]
N[Prometheus]
end
subgraph Collaborators
O[user-service via Feign]
P[payment-service via Feign]
end
A -->|JWT Bearer Token| J
J --> I
I --> C
I --> D
C --> E
D --> F
E --> O
E --> P
E --> G
F --> G
G -->|course.lifecycle.exchange| L
B -->|cnkart.order.exchange| L
L --> H
H --> E
E --> K
F --> K
E -.->|traces| M
E -.->|metrics| N
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/courses |
Public | List all courses with average ratings |
GET |
/courses/{id} |
Public | Get course by ID with average rating |
GET |
/courses/name/?name= |
Public | Search by name |
GET |
/courses/instructor/?instructor= |
Public | Search by instructor |
GET |
/courses/courseMaterial/?id= |
Public | Get materials for a course |
POST |
/courses |
🔒 Required | Create a new course |
PUT |
/courses/{id} |
🔒 Required | Update a course |
DELETE |
/courses/{id} |
🔒 Required | Delete a course |
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/courses/course/{courseId}/register/{userId} |
🔒 Required | Enroll a user (idempotent) |
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/progress |
🔒 Required | Mark a material as complete |
GET |
/progress/{userId} |
🔒 Required | Get all progress records for a user |
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/courses/{courseId}/rate |
🔒 Enrolled only | Submit a rating (1–5) with optional comment |
Enrollment is modelled as an explicit state machine persisted at every stage. This means failures are always traceable and duplicate requests are handled gracefully without double-charging.
stateDiagram-v2
[*] --> INITIATED : POST register
INITIATED --> USER_VERIFIED : Feign → user-service OK
INITIATED --> FAILED : user-service error
USER_VERIFIED --> PAYMENT_PENDING : preparing charge
PAYMENT_PENDING --> ENROLLED : Feign → payment-service OK
PAYMENT_PENDING --> FAILED : payment-service error
ENROLLED --> ENROLLED : duplicate request (idempotent)
FAILED --> FAILED : duplicate request (idempotent)
- API receives
{courseId, userId}. - Checks for an existing enrollment by
(userId, courseId)idempotency key. - If found → returns existing state, publishes
DUPLICATE_REQUESTevent. No double charge. - Creates
EnrollmentinINITIATEDstate. - Calls
user-servicevia Feign → transitions toUSER_VERIFIED. - Moves to
PAYMENT_PENDING, callspayment-servicevia Feign. - On success →
ENROLLED, publishesCONFIRMEDevent. - On any failure →
FAILED, publishesFAILED+NOTIFICATIONevents. Resilience4j fallback kicks in.
Students mark individual CourseMaterial items as completed. The system validates that the learner is actually enrolled before accepting a progress update.
POST /progress
Content-Type: application/json
Authorization: Bearer <token>
{
"userId": 42,
"materialId": 7,
"completed": true
}
GET /progress/42
→ [
{ "materialId": 7, "completed": true, "completedAt": "2026-07-17T10:00:00" },
{ "materialId": 8, "completed": false, "completedAt": null }
]
When a student marks the last remaining material of a course as complete, a CertificateEarned event is automatically published to RabbitMQ — no polling, no scheduled job.
sequenceDiagram
participant Student
participant ProgressController
participant CourseProgressService
participant CourseEventPublisher
participant RabbitMQ
Student->>ProgressController: POST /progress (last material)
ProgressController->>CourseProgressService: markComplete(userId, materialId)
CourseProgressService->>CourseProgressService: allMaterialsComplete?
CourseProgressService->>CourseEventPublisher: publishCertificateEarned(courseId, userId)
CourseEventPublisher->>RabbitMQ: CERTIFICATE.EARNED on course.lifecycle.exchange
Note over RabbitMQ: Downstream: email service, badge service, etc.
Peer reviews are gated by domain: only users with an ENROLLED status can submit a rating.
POST /courses/5/rate
Content-Type: application/json
Authorization: Bearer <token>
{
"userId": 42,
"rating": 5,
"comment": "Excellent course on distributed systems!"
}
The unique constraint (userId, courseId) prevents duplicate reviews. Average rating is aggregated from CourseRating and included in every course response:
{
"course": { "id": 5, "name": "Distributed Systems", "instructor": "Bipin Verma", ... },
"averageRating": 4.7
}| Exchange | Routing Key | Payload | Trigger |
|---|---|---|---|
course.lifecycle.exchange |
course.enrollment.initiated |
CourseEvent |
New enrollment created |
course.lifecycle.exchange |
course.enrollment.confirmed |
CourseEvent |
Payment succeeded |
course.lifecycle.exchange |
course.enrollment.failed |
CourseEvent |
Any step fails |
course.lifecycle.exchange |
course.payment.requested |
CourseEvent |
Pre-payment |
course.lifecycle.exchange |
course.notification.* |
CourseEvent |
Failure + duplicates |
course.lifecycle.exchange |
course.certificate.earned |
CourseEvent |
All materials complete |
Event Payload Shape:
{
"enrollmentId": 101,
"courseId": 5,
"userId": 42,
"correlationKey": "5_42",
"status": "CONFIRMED",
"message": "Enrollment successful",
"timestamp": "2026-07-17T10:00:00"
}sequenceDiagram
participant Client
participant CourseAPI as Course Service
participant UserSvc as user-service
participant PaySvc as payment-service
participant RabbitMQ
Client->>CourseAPI: POST /courses/course/{courseId}/register/{userId}
CourseAPI->>CourseAPI: Check idempotency key (userId + courseId)
alt Existing enrollment found
CourseAPI->>RabbitMQ: DUPLICATE_REQUEST event
CourseAPI-->>Client: 200 — existing state returned
else New enrollment
CourseAPI->>RabbitMQ: ENROLLMENT_INITIATED
CourseAPI->>UserSvc: GET user/{userId}
alt User not found
CourseAPI->>RabbitMQ: FAILED + NOTIFICATION events
CourseAPI-->>Client: 422 Unprocessable Entity
else User verified
CourseAPI->>RabbitMQ: USER_VERIFIED + PAYMENT_REQUESTED
CourseAPI->>PaySvc: POST payment
alt Payment succeeds
CourseAPI->>RabbitMQ: CONFIRMED event
CourseAPI-->>Client: 201 Created
else Payment fails
CourseAPI->>RabbitMQ: FAILED + NOTIFICATION events
CourseAPI-->>Client: 422 Unprocessable Entity
end
end
end
This service participates in cross-portfolio choreography. When a customer purchases a course bundle on the CNKart e-commerce platform, the EdTech service is automatically notified and triggers enrollment — with zero direct coupling between the two systems.
sequenceDiagram
participant Buyer
participant CNKart as CNKart Order Service
participant Broker as RabbitMQ Broker
participant EdTech as EdTech Course Service
participant DB as MySQL
Buyer->>CNKart: Place order for course bundle
CNKart->>Broker: Publish "order.confirmed" → cnkart.order.exchange
Broker-->>EdTech: OrderEventListener consumes from edtech.order.confirmed.queue
EdTech->>EdTech: Extract customerId from event payload
EdTech->>DB: createEnrollmentForCourse(bundleCourseId, customerId)
Note over EdTech,DB: Learner is automatically enrolled!
Listener binding:
Exchange : cnkart.order.exchange (type: topic)
Queue : edtech.order.confirmed.queue
Key : order.confirmedEvery request is tagged with a traceId and spanId that flow across all downstream Feign calls and RabbitMQ messages. Correlation IDs appear in every log line automatically:
2026-07-17 10:00:00 INFO [course-service,6a5346b258484bc2,c24080ee3168f33d] c.c.CourseService : Enrollment initiated for user 42 in course 5
Start Zipkin locally via Docker Compose and open http://localhost:9411:
docker compose up zipkin| Endpoint | Description |
|---|---|
GET /actuator/health |
Liveness and readiness |
GET /actuator/metrics |
All JVM + application metrics |
GET /actuator/prometheus |
Prometheus scrape endpoint |
Configure your prometheus.yml to scrape:
scrape_configs:
- job_name: 'course-service'
static_configs:
- targets: ['localhost:8081']
metrics_path: '/actuator/prometheus'The service is secured as an OAuth2 Resource Server expecting a JWT Bearer token on all write endpoints.
| Pattern | Rule |
|---|---|
GET /courses/** |
Public — no token required |
GET /actuator/**, /swagger-ui/** |
Public — ops and docs |
| All other requests | Authenticated — valid JWT required |
Every authenticated request is recorded with the caller's identity and the resource accessed:
SECURITY_AUDIT: User 'john.doe' with authorities '[ROLE_USER]' accessed POST /courses/course/5/register/42
This log is separate from the business log and gives a full access trail without touching AuditLog entities.
Add your issuer URI to application.yml or via environment variable:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${OAUTH2_ISSUER_URI:http://localhost:8080/realms/edtech}erDiagram
COURSE ||--o{ COURSE_MATERIAL : "has"
COURSE ||--o{ ENROLLMENT : "has"
COURSE ||--o{ COURSE_RATING : "has"
ENROLLMENT ||--o{ COURSE_PROGRESS : "tracks"
COURSE {
Long id PK
String name
String description
String instructor
Long amount
}
COURSE_MATERIAL {
Long id PK
Long course_id FK
String title
String content
}
ENROLLMENT {
Long id PK
Long user_id
Long course_id FK
EnrollmentStatus status
String correlationKey
}
COURSE_PROGRESS {
Long id PK
Long user_id
Long material_id FK
Boolean completed
LocalDateTime completedAt
}
COURSE_RATING {
Long id PK
Long user_id
Long course_id FK
int rating
String comment
}
(userId, courseId)is the idempotency key forEnrollment.(userId, courseId)has a unique constraint onCourseRating— one review per student per course.
Run the bundled load-test script (requires Apache Bench ab):
bash scripts/load-test.shResults — GET /courses, 50 concurrent users, 1000 requests:
| Metric | Value |
|---|---|
| Throughput | ~450 req/sec |
| Latency P50 | ~80 ms |
| Latency P95 | ~120 ms |
| Latency P99 | ~180 ms |
These numbers serve as the regression baseline. Future additions (e.g., a Redis course-catalog cache) should be benchmarked against these figures to quantify the improvement.
- Java 17
- Maven 3.8+
- Docker (for the infrastructure stack)
docker compose upThis starts MySQL (port 3306), RabbitMQ (port 5672 / management 15672), and Zipkin (port 9411).
| Variable | Default | Description |
|---|---|---|
DATASOURCE_URL |
jdbc:mysql://localhost:3306/edtech_course_service |
DB JDBC URL |
DATASOURCE_USERNAME |
root |
DB username |
DATASOURCE_PASSWORD |
changeme |
DB password |
RABBITMQ_USERNAME |
guest |
RabbitMQ user |
RABBITMQ_PASSWORD |
changeme |
RabbitMQ password |
USER_SERVICE_AUTH_TOKEN |
(empty) | Token for user-service Feign header |
OAUTH2_ISSUER_URI |
http://localhost:8080/realms/edtech |
JWT issuer |
mvn spring-boot:runThe API is available at http://localhost:8081.
Swagger UI: http://localhost:8081/swagger-ui/index.html
mvn clean verifyJaCoCo enforces ≥ 70% instruction coverage on the service layer. The build fails below this threshold.
target/site/jacoco/index.html ← HTML coverage report
| Layer | Type | What Is Tested |
|---|---|---|
CourseServiceTest |
Unit (Mockito) | Enrollment state machine, duplicate handling, rating guards |
CourseEventPublisherTest |
Unit (Mockito) | RabbitMQ payload construction for all event types |
CourseServiceIntegrationTest |
Spring @SpringBootTest |
Full Spring context wired with mocked Feign clients |
CourseControllerIntegrationTest |
Spring @WebMvcTest |
HTTP routing, request/response mapping, status codes |
FullIntegrationTest |
Testcontainers | Real MySQL + RabbitMQ — happy-path end-to-end |
FullIntegrationTestuses@Testcontainersand@DynamicPropertySourceto wire real containers with zero manual setup. Enable it by running with a live Docker daemon.
| Category | Technology | Version |
|---|---|---|
| Runtime | Java | 17 |
| Framework | Spring Boot | 2.7.13 |
| Cloud BOM | Spring Cloud | 2021.0.8 |
| API | Spring Web MVC | (Boot-managed) |
| API Docs | SpringDoc OpenAPI UI | 1.7.0 |
| Persistence | Spring Data JPA + Hibernate | (Boot-managed) |
| Database | MySQL | 8 (dialect: MySQL8Dialect) |
| Messaging | Spring AMQP (RabbitMQ) | (Boot-managed) |
| Service Discovery | Spring Cloud Netflix Eureka Client | (Cloud BOM) |
| HTTP Clients | Spring Cloud OpenFeign, RestTemplate | (Cloud BOM) |
| Resilience | Resilience4j circuit-breaker + retry | (Cloud BOM) |
| Security | Spring Security + OAuth2 Resource Server | (Boot-managed) |
| Observability | Spring Cloud Sleuth + Zipkin | (Cloud BOM) |
| Metrics | Micrometer + Prometheus registry | (Boot-managed) |
| Testing | JUnit 5, Mockito, Spring Test | (Boot-managed) |
| Testcontainers | MySQL + RabbitMQ containers | 1.19.3 |
| Coverage | JaCoCo (≥70% on service layer) | 0.8.11 |
| Build | Maven + GitHub Actions CI | 3.x |
| Utilities | Lombok | 1.18.30 |
java · spring-boot · spring-cloud · lms · microservices · event-driven · rabbitmq · oauth2 · jwt · idempotency · resilience4j · feign · jpa · mysql · testcontainers · zipkin · micrometer · prometheus · rest-api