A comprehensive guide to testing the Notification System API using HTTPie - the human-friendly command-line HTTP client. This guide provides detailed explanations of each endpoint, request/response formats, and testing workflows.
- What is HTTPie?
- Prerequisites
- HTTPie Installation & Setup
- HTTPie Syntax Guide
- Test Data Reference
- Health Check Endpoints
- Notification Endpoints
- Template Endpoints
- User Endpoints
- OpenAPI Documentation
- Testing Workflows
- Error Handling
- Troubleshooting
- Shell Variables for Testing
HTTPie (pronounced "aitch-tee-tee-pie") is a modern command-line HTTP client designed for testing APIs. It's an alternative to curl with several advantages:
| Feature | HTTPie | curl |
|---|---|---|
| JSON by default | ✅ Automatic | ❌ Manual -H "Content-Type: application/json" |
| Syntax | http POST :8080/api key=value |
curl -X POST -H "..." -d '{"key":"value"}' |
| Output | Colored, formatted | Raw text |
| Headers | Header:Value |
-H "Header: Value" |
| Learning curve | Easy | Steep |
HTTPie:
http POST :8080/api/v1/notifications userId=abc channel=EMAIL content="Hello"Equivalent curl:
curl -X POST http://localhost:8080/api/v1/notifications \
-H "Content-Type: application/json" \
-d '{"userId":"abc","channel":"EMAIL","content":"Hello"}'Before testing, ensure all Docker services are running:
# Open Docker Desktop first (required on macOS/Windows)
open -a Docker # macOS only
# Wait for Docker to start, then run:
docker compose up -d
# Verify all containers are running
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"Expected Running Containers:
| Container | Image | Port | Purpose | Health Check |
|---|---|---|---|---|
notification-postgres |
postgres:15 | 5432 | Primary database | pg_isready |
notification-redis |
redis:7-alpine | 6379 | Rate limiting cache | redis-cli ping |
notification-kafka-1 |
confluentinc/cp-kafka:7.4.0 | 9092 | Message queue (Broker 1) | Topic creation |
notification-kafka-2 |
confluentinc/cp-kafka:7.4.0 | 9093 | Message queue (Broker 2) | Topic creation |
notification-kafka-3 |
confluentinc/cp-kafka:7.4.0 | 9094 | Message queue (Broker 3) | Topic creation |
notification-zookeeper |
confluentinc/cp-zookeeper:7.4.0 | 2181 | Kafka coordination | ZK client |
notification-kafka-ui |
provectuslabs/kafka-ui:latest | 8090 | Kafka monitoring UI | HTTP |
# Navigate to project directory
cd /path/to/notification-system
# Start Spring Boot application
mvn spring-boot:runWait for this log message:
Started NotificationSystemApplication in X.XXX seconds
Application URLs:
| Service | URL |
|---|---|
| API Base | http://localhost:8080 |
| Swagger UI | http://localhost:8080/swagger-ui.html |
| Actuator | http://localhost:8080/actuator |
| Kafka UI | http://localhost:8090 |
macOS (Homebrew):
brew install httpieLinux (apt):
sudo apt install httpieLinux (pip):
pip install httpieWindows (pip):
pip install httpieVerify Installation:
http --version
# Output: 3.x.xCreate a config file for consistent behavior:
# Create HTTPie config directory
mkdir -p ~/.config/httpie
# Create config file with defaults
cat > ~/.config/httpie/config.json << 'EOF'
{
"default_options": ["--style=monokai", "--print=hHbB"]
}
EOFConfig Options Explained:
--style=monokai: Colorful syntax highlighting--print=hHbB: Show request headers (h), request body (b), response headers (H), response body (B)
http [METHOD] URL [REQUEST_ITEMS...]| Method | HTTPie Syntax | Purpose |
|---|---|---|
| GET | http GET :8080/path or http :8080/path |
Retrieve data |
| POST | http POST :8080/path |
Create resource |
| PUT | http PUT :8080/path |
Update resource |
| DELETE | http DELETE :8080/path |
Delete resource |
| PATCH | http PATCH :8080/path |
Partial update |
| Syntax | Type | Example |
|---|---|---|
key=value |
JSON string | name=John → {"name": "John"} |
key:=value |
JSON non-string | count:=5 → {"count": 5} |
key:=true |
JSON boolean | active:=true → {"active": true} |
key:='["a","b"]' |
JSON array | items:='["a","b"]' → {"items": ["a","b"]} |
Header:Value |
HTTP Header | Accept:application/json |
key==value |
Query parameter | page==0 → ?page=0 |
# These are equivalent:
http GET http://localhost:8080/api/v1/health
http GET localhost:8080/api/v1/health
http GET :8080/api/v1/health
http :8080/api/v1/health # GET is default| Flag | Description |
|---|---|
--print=h |
Request headers only |
--print=b |
Request body only |
--print=H |
Response headers only |
--print=B |
Response body only |
--print=hb |
Request headers + body |
--print=HB |
Response headers + body (default) |
-v or --verbose |
Show everything |
-b or --body |
Response body only (shortcut) |
-h or --headers |
Response headers only (shortcut) |
| Flag | Description |
|---|---|
--json or -j |
Force JSON (default) |
--form or -f |
Form data instead of JSON |
--pretty=all |
Format and colorize output |
--pretty=none |
Raw output (for piping) |
--timeout=30 |
Set timeout in seconds |
--follow |
Follow redirects |
--offline |
Build request without sending |
The database is pre-populated with test users via Flyway migrations:
| Variable | User ID | Phone | Device Token | |
|---|---|---|---|---|
USER_1 |
550e8400-e29b-41d4-a716-446655440001 |
john.doe@example.com | +1234567890 | device_token_user1 |
USER_2 |
550e8400-e29b-41d4-a716-446655440002 |
jane.smith@example.com | +0987654321 | device_token_user2 |
| Name | Channel | Template ID | Variables |
|---|---|---|---|
welcome-email |
660e8400-e29b-41d4-a716-446655440001 |
{{userName}} |
|
order-confirmation |
660e8400-e29b-41d4-a716-446655440002 |
{{userName}}, {{orderId}}, {{orderTotal}} |
|
otp-sms |
SMS | 660e8400-e29b-41d4-a716-446655440003 |
{{otpCode}} |
order-shipped |
PUSH | 660e8400-e29b-41d4-a716-446655440004 |
{{orderId}}, {{trackingUrl}} |
Each notification channel routes to its own Kafka topic (Alex Xu's design pattern):
| Channel | Kafka Topic | Partitions | Consumer Group | Use Case |
|---|---|---|---|---|
EMAIL |
notifications.email |
3 | notification-consumer-group-email | Email notifications |
SMS |
notifications.sms |
2 | notification-consumer-group-sms | Text messages |
PUSH |
notifications.push |
4 | notification-consumer-group-push | Mobile push |
IN_APP |
notifications.in-app |
3 | notification-consumer-group-inapp | In-app alerts |
| - | notifications.dlq |
1 | - | Dead Letter Queue |
Why Separate Topics?
- Independent scaling per channel
- Email delays don't affect push notifications
- Different consumer counts based on volume
- Isolated failure domains
| Priority | Value | Processing Order | Use Case |
|---|---|---|---|
CRITICAL |
0 | First | OTP, security alerts |
HIGH |
1 | Second | Account alerts, important updates |
MEDIUM |
2 | Third (default) | Order updates, general notifications |
LOW |
3 | Last | Marketing, promotional content |
┌─────────────────────────────────────────────────────────────────┐
│ NOTIFICATION LIFECYCLE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐│
│ │ PENDING │───▶│ SENT │───▶│ DELIVERED │───▶│ READ ││
│ └──────────┘ └──────────┘ └───────────┘ └──────────┘│
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ FAILED │──(retry if retryCount < 3)──▶ PENDING │
│ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
| Status | Description | Duration |
|---|---|---|
PENDING |
Queued in Kafka, waiting for consumer | < 1 second typically |
SENT |
Delivered to channel handler (email server, SMS gateway) | Immediate |
DELIVERED |
Confirmed receipt by end system | Depends on channel |
READ |
User acknowledged/opened notification | User-dependent |
FAILED |
Delivery failed, may retry | Up to 3 retries |
Health endpoints verify the API and its dependencies are operational.
Purpose: Quick verification that the API is responding.
Request:
http :8080/api/v1/healthWhat Happens:
- Request hits
HealthController.healthCheck() - Returns simple "OK" without checking dependencies
- Fast response (< 10ms)
Expected Response (200 OK):
{
"success": true,
"message": "Service is healthy",
"data": "OK",
"timestamp": "2026-01-10T10:30:00.000000+05:30"
}Response Fields:
| Field | Type | Description |
|---|---|---|
success |
boolean | true if request succeeded |
message |
string | Human-readable status message |
data |
string | Health status ("OK") |
timestamp |
ISO 8601 | Server timestamp with timezone |
When to Use:
- Load balancer health probes
- Quick "is it up?" checks
- Kubernetes liveness probes
Purpose: Comprehensive health check of all dependencies.
Request:
http :8080/actuator/healthWhat Happens:
- Spring Boot Actuator checks each component
- PostgreSQL: Executes validation query
- Redis: Sends PING command
- Kafka: Verifies broker connectivity
- Aggregates all statuses
Expected Response (200 OK):
{
"status": "UP",
"components": {
"db": {
"status": "UP",
"details": {
"database": "PostgreSQL",
"validationQuery": "isValid()"
}
},
"diskSpace": {
"status": "UP",
"details": {
"total": 499963174912,
"free": 123456789012,
"threshold": 10485760
}
},
"kafka": {
"status": "UP"
},
"ping": {
"status": "UP"
},
"redis": {
"status": "UP"
}
}
}Status Values:
| Status | Meaning | HTTP Code |
|---|---|---|
UP |
Component is healthy | 200 |
DOWN |
Component is unhealthy | 503 |
OUT_OF_SERVICE |
Component is offline intentionally | 503 |
UNKNOWN |
Status cannot be determined | 200 |
When to Use:
- Kubernetes readiness probes
- Monitoring dashboards
- Debugging connectivity issues
Troubleshooting Failed Components:
# If db is DOWN:
docker logs notification-postgres
# If redis is DOWN:
docker exec notification-redis redis-cli ping
# If kafka is DOWN:
docker logs notification-kafkaPurpose: Retrieve application metadata and build information.
Request:
http :8080/actuator/infoExpected Response (200 OK):
{
"app": {
"name": "Notification System",
"description": "Multi-channel notification service",
"version": "1.0.0"
}
}When to Use:
- Verify deployed version
- CI/CD pipeline validation
- Documentation purposes
Notification endpoints handle sending, retrieving, and managing notifications across all channels.
Purpose: Send an email using a pre-defined template with variable substitution.
Endpoint Details:
| Property | Value |
|---|---|
| Method | POST |
| URL | /api/v1/notifications |
| Content-Type | application/json |
| Kafka Topic | notifications.email |
| Partitions | 3 |
Request:
http POST :8080/api/v1/notifications \
userId=550e8400-e29b-41d4-a716-446655440001 \
channel=EMAIL \
templateName=welcome-email \
templateVariables:='{"userName": "John Doe"}' \
priority=HIGH \
eventId=user-registration-12345Request Body Breakdown:
| Field | Type | Required | Validation | Description |
|---|---|---|---|---|
userId |
UUID | ✅ Yes | Must exist in users table |
Target user's ID |
channel |
Enum | ✅ Yes | EMAIL, SMS, PUSH, IN_APP | Delivery channel |
templateName |
String | Must exist and be active | Template to use | |
templateVariables |
Object | ❌ No | Keys must match template variables | Variable values |
priority |
Enum | ❌ No | LOW, MEDIUM, HIGH, CRITICAL | Default: MEDIUM |
eventId |
String | ❌ No | Max 255 chars, unique per event | Event identifier for deduplication |
What Happens Internally:
1. Controller receives request
└─▶ NotificationController.sendNotification()
2. Service layer processing
└─▶ NotificationService.sendNotification()
├─▶ Check deduplication (DeduplicationService.isDuplicate)
│ └─▶ If duplicate: return FAILED status immediately
├─▶ Validate user exists (UserRepository.findById)
├─▶ Check rate limit (RateLimiterService - Token Bucket)
├─▶ Load template (NotificationTemplateRepository.findByNameAndActive)
├─▶ Render template (replace {{variables}})
└─▶ Create Notification entity (status: PENDING)
3. Kafka publishing
└─▶ KafkaTemplate.send("notifications.email", notificationId)
4. Async consumer processing
└─▶ NotificationConsumer.processEmailNotification()
├─▶ Load notification from DB
├─▶ Dispatch to EmailChannelHandler
├─▶ Update status to SENT
└─▶ Save sentAt timestamp
Expected Response (200 OK):
{
"success": true,
"message": "Notification queued successfully",
"data": {
"id": "c57aaec7-80a4-4948-84b8-6d9582737410",
"userId": "550e8400-e29b-41d4-a716-446655440001",
"channel": "EMAIL",
"priority": "HIGH",
"subject": "Welcome to Our Platform, John Doe!",
"content": "Hi John Doe,\n\nThank you for joining our platform!...",
"status": "PENDING",
"retryCount": 0,
"createdAt": "2026-01-10T10:30:00.000000+05:30"
},
"timestamp": "2026-01-10T10:30:00.000000+05:30"
}Response Fields:
| Field | Type | Description |
|---|---|---|
id |
UUID | Unique notification identifier for tracking |
userId |
UUID | Target user (copied from request) |
channel |
String | Delivery channel used |
priority |
String | Priority level |
subject |
String | Rendered subject (template variables replaced) |
content |
String | Rendered body (template variables replaced) |
status |
String | Initial status is always PENDING |
retryCount |
Integer | Starts at 0, increments on failure |
createdAt |
ISO 8601 | Creation timestamp |
Verify in Kafka UI:
- Open http://localhost:8090
- Navigate to Topics →
notifications.email - View Messages → You'll see the notification ID
Purpose: Send an email with custom subject and body without using a template.
Request:
http POST :8080/api/v1/notifications \
userId=550e8400-e29b-41d4-a716-446655440001 \
channel=EMAIL \
subject="Custom Email Subject" \
content="This is a custom email without a template. You can include any content here." \
priority=MEDIUMRequest Body Breakdown:
| Field | Type | Required | Description |
|---|---|---|---|
userId |
UUID | ✅ Yes | Target user's ID |
channel |
Enum | ✅ Yes | Must be EMAIL |
subject |
String | ✅ Yes* | Email subject line |
content |
String | ✅ Yes* | Email body text |
priority |
Enum | ❌ No | Default: MEDIUM |
*Either templateName OR (subject + content) must be provided.
When to Use:
- One-off notifications
- Dynamic content that doesn't fit templates
- Testing and debugging
Purpose: Send a time-sensitive SMS, typically for OTP verification.
Endpoint Details:
| Property | Value |
|---|---|
| Kafka Topic | notifications.sms |
| Partitions | 2 |
| Typical Latency | < 500ms |
Request:
http POST :8080/api/v1/notifications \
userId=550e8400-e29b-41d4-a716-446655440001 \
channel=SMS \
templateName=otp-sms \
templateVariables:='{"otpCode": "847293"}' \
priority=CRITICAL \
eventId=otp-request-67890Why CRITICAL Priority?
- OTP codes typically expire in 5-10 minutes
- User is actively waiting for the code
- Bypasses normal queue ordering
- Processed before LOW/MEDIUM/HIGH messages
SMS Template Rendering:
Template: "Your OTP code is {{otpCode}}. Valid for 5 minutes."
Params: {"otpCode": "847293"}
Result: "Your OTP code is 847293. Valid for 5 minutes."
Expected Response (200 OK):
{
"success": true,
"message": "Notification queued successfully",
"data": {
"id": "abc123...",
"userId": "550e8400-e29b-41d4-a716-446655440001",
"channel": "SMS",
"priority": "CRITICAL",
"subject": null,
"content": "Your OTP code is 847293. Valid for 5 minutes.",
"status": "PENDING",
"retryCount": 0,
"createdAt": "2026-01-10T10:30:00.000000+05:30"
},
"timestamp": "2026-01-10T10:30:00.000000+05:30"
}Note: SMS notifications don't have a subject field (always null).
Purpose: Send a push notification to user's mobile device.
Endpoint Details:
| Property | Value |
|---|---|
| Kafka Topic | notifications.push |
| Partitions | 4 (highest due to volume) |
Request:
http POST :8080/api/v1/notifications \
userId=550e8400-e29b-41d4-a716-446655440002 \
channel=PUSH \
templateName=order-shipped \
templateVariables:='{"orderId": "ORD-12345", "trackingUrl": "https://track.example.com/ORD-12345"}' \
priority=HIGHWhy 4 Partitions for Push?
- Push notifications typically have the highest volume
- More partitions = more parallel consumers
- Each partition can process independently
- Scales to millions of notifications per day
Push Notification Structure:
{
"title": "Your order has shipped!", // from subjectTemplate
"body": "Order ORD-12345 is on its way", // from bodyTemplate
"data": {
"orderId": "ORD-12345",
"trackingUrl": "https://track.example.com/ORD-12345"
}
}Purpose: Send a notification to user's in-app notification center.
Endpoint Details:
| Property | Value |
|---|---|
| Kafka Topic | notifications.in-app |
| Partitions | 3 |
Request:
http POST :8080/api/v1/notifications \
userId=550e8400-e29b-41d4-a716-446655440001 \
channel=IN_APP \
subject="New Feature Available!" \
content="Check out our new dashboard feature with enhanced analytics." \
priority=LOW \
eventId=feature-announcement-11111In-App Notification Characteristics:
- Stored in database for persistent retrieval
- User sees them on next app visit
- Typically displayed as badge/bell icon
- LOW priority appropriate (not time-sensitive)
- Can be marked as READ by user
Expected Response (200 OK):
{
"success": true,
"message": "Notification queued successfully",
"data": {
"id": "def456...",
"channel": "IN_APP",
"priority": "LOW",
"subject": "New Feature Available!",
"content": "Check out our new dashboard feature with enhanced analytics.",
"status": "PENDING"
}
}Purpose: Send the same notification to multiple users simultaneously.
Request:
http POST :8080/api/v1/notifications/bulk \
userIds:='["550e8400-e29b-41d4-a716-446655440001", "550e8400-e29b-41d4-a716-446655440002"]' \
channel=EMAIL \
templateName=order-confirmation \
templateVariables:='{"orderId": "ORD-99999", "orderTotal": "$149.99", "userName": "Valued Customer"}' \
priority=HIGH \
eventId=bulk-order-confirmation-99999Request Body Breakdown:
| Field | Type | Required | Max Size | Description |
|---|---|---|---|---|
userIds |
UUID[] | ✅ Yes | 1000 | Array of target user IDs |
channel |
Enum | ✅ Yes | - | Single channel for all |
templateName |
String | - | Template to use | |
templateVariables |
Object | ❌ No | - | Same params for all users |
priority |
Enum | ❌ No | - | Default: MEDIUM |
eventId |
String | ❌ No | 255 chars | Event identifier for deduplication |
What Happens Internally:
For each userId in userIds:
├─▶ Check deduplication (DeduplicationService.isDuplicate)
│ └─▶ If duplicate: skip user, increment failed count
├─▶ Validate user exists
├─▶ Check rate limit
├─▶ Create individual notification
└─▶ Publish to Kafka topic
Returns aggregate results
Expected Response (200 OK):
{
"success": true,
"message": "Bulk notifications queued",
"data": {
"totalRequested": 2,
"successCount": 2,
"failedCount": 0,
"notificationIds": [
"uuid-1-for-user1",
"uuid-2-for-user2"
],
"failedUserIds": []
},
"timestamp": "2026-01-10T10:30:00.000000+05:30"
}Response Fields:
| Field | Type | Description |
|---|---|---|
totalRequested |
Integer | Number of users in request |
successCount |
Integer | Successfully queued |
failedCount |
Integer | Failed to queue |
notificationIds |
UUID[] | IDs of created notifications |
failedUserIds |
UUID[] | Users that failed (rate limit, not found) |
Use Cases:
- Marketing campaigns
- System-wide announcements
- Promotional offers
- Service disruption notices
Purpose: Retrieve details and current status of a specific notification.
Request:
http :8080/api/v1/notifications/c57aaec7-80a4-4948-84b8-6d9582737410Expected Response (200 OK):
{
"success": true,
"message": "Notification retrieved",
"data": {
"id": "c57aaec7-80a4-4948-84b8-6d9582737410",
"userId": "550e8400-e29b-41d4-a716-446655440001",
"channel": "EMAIL",
"priority": "HIGH",
"subject": "Welcome to Our Platform, John Doe!",
"content": "Hi John Doe,\n\nThank you for joining...",
"status": "SENT",
"retryCount": 0,
"createdAt": "2026-01-10T10:30:00.000000+05:30",
"sentAt": "2026-01-10T10:30:01.000000+05:30",
"deliveredAt": null,
"readAt": null,
"errorMessage": null
},
"timestamp": "2026-01-10T10:30:05.000000+05:30"
}Additional Response Fields:
| Field | Type | Description |
|---|---|---|
sentAt |
ISO 8601 | When notification was sent to channel |
deliveredAt |
ISO 8601 | When delivery was confirmed |
readAt |
ISO 8601 | When user read the notification |
errorMessage |
String | Error details if FAILED |
Error Response (404 Not Found):
{
"success": false,
"message": "Notification not found with id: invalid-uuid",
"timestamp": "2026-01-10T10:30:00.000000+05:30"
}Purpose: Retrieve all notifications for a specific user with pagination.
Request:
http :8080/api/v1/notifications/user/550e8400-e29b-41d4-a716-446655440001 \
page==0 \
size==10Query Parameters:
| Parameter | Type | Default | Max | Description |
|---|---|---|---|---|
page |
Integer | 0 | - | Page number (0-indexed) |
size |
Integer | 20 | 100 | Items per page |
status |
Enum | - | - | Optional filter |
Expected Response (200 OK):
{
"success": true,
"message": "Notifications retrieved",
"data": {
"content": [
{
"id": "uuid-1",
"channel": "EMAIL",
"priority": "HIGH",
"subject": "Welcome!",
"status": "SENT",
"createdAt": "2026-01-10T10:30:00.000000+05:30"
},
{
"id": "uuid-2",
"channel": "SMS",
"priority": "CRITICAL",
"subject": null,
"status": "DELIVERED",
"createdAt": "2026-01-10T10:25:00.000000+05:30"
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 10,
"sort": {
"sorted": true,
"orderBy": "createdAt DESC"
}
},
"totalElements": 25,
"totalPages": 3,
"first": true,
"last": false
},
"timestamp": "2026-01-10T10:30:00.000000+05:30"
}Pagination Fields:
| Field | Type | Description |
|---|---|---|
content |
Array | Notifications for current page |
pageNumber |
Integer | Current page (0-indexed) |
pageSize |
Integer | Items per page |
totalElements |
Integer | Total notifications for user |
totalPages |
Integer | Total pages available |
first |
Boolean | Is this the first page? |
last |
Boolean | Is this the last page? |
Purpose: Get only notifications with a specific status.
Request:
# Get PENDING notifications
http :8080/api/v1/notifications/user/550e8400-e29b-41d4-a716-446655440001 \
page==0 \
size==10 \
status==PENDING
# Get FAILED notifications (for troubleshooting)
http :8080/api/v1/notifications/user/550e8400-e29b-41d4-a716-446655440001 \
status==FAILEDAvailable Status Filters:
| Status | When to Use |
|---|---|
PENDING |
Check queued but unprocessed |
SENT |
View successfully sent |
DELIVERED |
Confirmed deliveries |
FAILED |
Troubleshoot issues |
READ |
Track engagement |
Templates define reusable message formats with {{variable}} placeholders.
Purpose: List all notification templates (active and inactive).
Request:
http :8080/api/v1/templatesExpected Response (200 OK):
{
"success": true,
"message": "Templates retrieved",
"data": [
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"name": "welcome-email",
"channel": "EMAIL",
"subjectTemplate": "Welcome to Our Platform, {{userName}}!",
"bodyTemplate": "Hi {{userName}},\n\nThank you for joining our platform!...",
"active": true,
"createdAt": "2026-01-01T00:00:00.000000+05:30",
"updatedAt": "2026-01-01T00:00:00.000000+05:30"
},
{
"id": "660e8400-e29b-41d4-a716-446655440003",
"name": "otp-sms",
"channel": "SMS",
"subjectTemplate": null,
"bodyTemplate": "Your OTP code is {{otpCode}}. Valid for 5 minutes.",
"active": true,
"createdAt": "2026-01-01T00:00:00.000000+05:30",
"updatedAt": "2026-01-01T00:00:00.000000+05:30"
}
],
"timestamp": "2026-01-10T10:30:00.000000+05:30"
}Template Fields:
| Field | Type | Description |
|---|---|---|
id |
UUID | Unique template identifier |
name |
String | Unique name used in API calls |
channel |
Enum | EMAIL, SMS, PUSH, IN_APP |
subjectTemplate |
String | Subject with placeholders (null for SMS) |
bodyTemplate |
String | Body with placeholders |
active |
Boolean | Can be used for new notifications |
Request:
http :8080/api/v1/templates/660e8400-e29b-41d4-a716-446655440001Request:
http :8080/api/v1/templates/name/welcome-emailPurpose: Create a new email notification template.
Request:
http POST :8080/api/v1/templates \
name=password-reset \
channel=EMAIL \
subjectTemplate="Password Reset Request - {{appName}}" \
bodyTemplate="Hi {{userName}},\n\nWe received a request to reset your password.\n\nClick the link below to reset it:\n{{resetLink}}\n\nThis link will expire in {{expiryTime}}.\n\nIf you didn't request this, please ignore this email.\n\nBest regards,\nThe {{appName}} Team" \
active:=trueRequest Body Breakdown:
| Field | Type | Required | Validation | Description |
|---|---|---|---|---|
name |
String | ✅ Yes | Unique, no spaces | Template identifier |
channel |
Enum | ✅ Yes | EMAIL, SMS, PUSH, IN_APP | Target channel |
subjectTemplate |
String | ❌ No* | - | Subject with {{vars}} |
bodyTemplate |
String | ✅ Yes | - | Body with {{vars}} |
active |
Boolean | ❌ No | - | Default: true |
*Subject is required for EMAIL and recommended for PUSH/IN_APP.
Template Variable Syntax:
- Use
{{variableName}}for placeholders - Variable names are case-sensitive
- Variables must be provided in
templateVariableswhen sending - Missing variables will appear as literal
{{variableName}}
Expected Response (201 Created):
{
"success": true,
"message": "Template created",
"data": {
"id": "generated-uuid-here",
"name": "password-reset",
"channel": "EMAIL",
"subjectTemplate": "Password Reset Request - {{appName}}",
"bodyTemplate": "Hi {{userName}},...",
"active": true,
"createdAt": "2026-01-10T10:30:00.000000+05:30"
},
"timestamp": "2026-01-10T10:30:00.000000+05:30"
}Request:
http POST :8080/api/v1/templates \
name=delivery-eta-sms \
channel=SMS \
bodyTemplate="Your order {{orderId}} will arrive today between {{timeSlot}}. Track: {{trackingUrl}}" \
active:=trueSMS Template Notes:
- No
subjectTemplate(SMS doesn't support subjects) - Keep under 160 characters when possible
- Consider character limits for variables
Request:
http POST :8080/api/v1/templates \
name=flash-sale-push \
channel=PUSH \
subjectTemplate="🔥 Flash Sale Alert!" \
bodyTemplate="{{discount}}% OFF on {{category}}! Ends in {{hours}} hours. Shop now!" \
active:=truePush Template Notes:
- Emojis supported in subject
- Keep content concise (notification banner limit)
- Subject becomes push notification title
Request:
http POST :8080/api/v1/templates \
name=account-activity-alert \
channel=IN_APP \
subjectTemplate="Security Alert" \
bodyTemplate="New login detected from {{deviceType}} in {{location}} at {{loginTime}}. If this wasn't you, please secure your account." \
active:=truePurpose: Modify an existing template.
Request:
http PUT :8080/api/v1/templates/660e8400-e29b-41d4-a716-446655440001 \
name=welcome-email \
channel=EMAIL \
subjectTemplate="Welcome to Our Platform, {{userName}}! 🎉" \
bodyTemplate="Hi {{userName}},\n\nWelcome aboard! We're thrilled to have you.\n\nHere's what you can do next:\n1. Complete your profile\n2. Explore features\n3. Connect with others\n\nNeed help? Contact support.\n\nBest regards,\nThe Team" \
active:=trueNote: All fields are required for PUT (full update). For partial updates, use the same values for unchanged fields.
Purpose: Soft-delete a template (sets active to false).
Request:
http DELETE :8080/api/v1/templates/660e8400-e29b-41d4-a716-446655440004Expected Response (200 OK):
{
"success": true,
"message": "Template deleted",
"timestamp": "2026-01-10T10:30:00.000000+05:30"
}Note: This is a soft delete:
- Template remains in database
activeset tofalse- Cannot be used for new notifications
- Historical notifications still reference it
The User endpoints provide cached user lookups for testing Redis caching functionality. These endpoints demonstrate the caching implementation following Alex Xu's system design principles.
Endpoint: GET /api/v1/users/email/{email}
Purpose: Retrieve user information by email address with Redis caching.
Caching: First request hits database and caches result. Subsequent requests serve from Redis cache without database queries.
# Test with existing user
http :8080/api/v1/users/email/john@example.com
# Test with non-existent user (will throw exception, not cached)
http :8080/api/v1/users/email/nonexistent@example.comResponse (Success):
{
"success": true,
"message": "User found",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440001",
"email": "john@example.com",
"phone": "+1234567890",
"deviceToken": "device_token_123",
"createdAt": "2024-01-15T10:00:00Z",
"updatedAt": "2024-01-15T10:00:00Z"
},
"timestamp": "2024-01-15T10:30:00Z"
}Endpoint: GET /api/v1/users/phone/{phone}
Purpose: Retrieve user information by phone number with Redis caching.
Caching: First request hits database and caches result. Subsequent requests serve from Redis cache.
# Test with existing user
http :8080/api/v1/users/phone/+1234567890
# Test with non-existent user
http :8080/api/v1/users/phone/+9999999999Endpoint: GET /api/v1/users/push-eligible
Purpose: Retrieve all users with device tokens for push notifications.
Caching: Caches the list of users who can receive push notifications to avoid repeated database queries.
http :8080/api/v1/users/push-eligibleResponse:
{
"success": true,
"message": "Push-eligible users retrieved",
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"email": "john@example.com",
"phone": "+1234567890",
"deviceToken": "device_token_123",
"createdAt": "2024-01-15T10:00:00Z",
"updatedAt": "2024-01-15T10:00:00Z"
}
],
"timestamp": "2024-01-15T10:30:00Z"
}Cache Testing Workflow:
-
Clear Redis cache:
docker exec notification-redis redis-cli FLUSHALL -
First request (cache miss - hits database):
http :8080/api/v1/users/email/john@example.com
-
Check Redis cache:
docker exec notification-redis redis-cli keys "*" # Should show: users::email:john@example.com
-
Second request (cache hit - serves from Redis):
http :8080/api/v1/users/email/john@example.com
-
Verify no additional database queries by checking application logs.
http :8080/v3/api-docshttp :8080/v3/api-docs.yaml Accept:application/x-yamlOpen in browser: http://localhost:8080/swagger-ui.html
Test the entire flow from health check to delivery:
# Step 1: Verify API health
http :8080/api/v1/health
# Step 2: Check dependencies
http :8080/actuator/health
# Step 3: List available templates
http :8080/api/v1/templates
# Step 4: Send notification
RESPONSE=$(http POST :8080/api/v1/notifications \
userId=550e8400-e29b-41d4-a716-446655440001 \
channel=EMAIL \
templateName=welcome-email \
templateVariables:='{"userName": "Test User"}' \
priority=HIGH --print=b)
echo "$RESPONSE"
# Step 5: Extract notification ID and check status
NOTIFICATION_ID=$(echo "$RESPONSE" | jq -r '.data.id')
echo "Notification ID: $NOTIFICATION_ID"
# Step 6: Wait and check status
sleep 2
http :8080/api/v1/notifications/$NOTIFICATION_ID# Step 1: Create a new template
http POST :8080/api/v1/templates \
name=payment-success \
channel=EMAIL \
subjectTemplate="Payment Received - {{amount}}" \
bodyTemplate="Hi {{userName}},\n\nWe received your payment of {{amount}}.\n\nTransaction ID: {{transactionId}}\nDate: {{paymentDate}}\n\nThank you!" \
active:=true
# Step 2: Verify creation
http :8080/api/v1/templates/name/payment-success
# Step 3: Use the new template
http POST :8080/api/v1/notifications \
userId=550e8400-e29b-41d4-a716-446655440001 \
channel=EMAIL \
templateName=payment-success \
templateVariables:='{"userName": "John Doe", "amount": "$99.99", "transactionId": "TXN-123456", "paymentDate": "January 10, 2026"}' \
priority=HIGHecho "=== Testing EMAIL ==="
http POST :8080/api/v1/notifications \
userId=550e8400-e29b-41d4-a716-446655440001 \
channel=EMAIL \
content="Test email notification" \
priority=HIGH --print=b | jq '.data.id'
echo "=== Testing SMS ==="
http POST :8080/api/v1/notifications \
userId=550e8400-e29b-41d4-a716-446655440001 \
channel=SMS \
content="Test SMS notification" \
priority=HIGH --print=b | jq '.data.id'
echo "=== Testing PUSH ==="
http POST :8080/api/v1/notifications \
userId=550e8400-e29b-41d4-a716-446655440001 \
channel=PUSH \
subject="Test Push" \
content="Test push notification" \
priority=HIGH --print=b | jq '.data.id'
echo "=== Testing IN_APP ==="
http POST :8080/api/v1/notifications \
userId=550e8400-e29b-41d4-a716-446655440001 \
channel=IN_APP \
subject="Test In-App" \
content="Test in-app notification" \
priority=HIGH --print=b | jq '.data.id'
echo "=== Check Kafka UI at http://localhost:8090 ==="# Rate limit is 10 per minute per user per channel
for i in {1..12}; do
echo "Request $i:"
http POST :8080/api/v1/notifications \
userId=550e8400-e29b-41d4-a716-446655440001 \
channel=EMAIL \
subject="Rate Limit Test $i" \
content="Testing rate limiting" \
priority=LOW --print=b | jq '{success: .success, message: .message}'
sleep 1
doneExpected: First 10 succeed, requests 11-12 fail with rate limit error.
# Send to both test users
http POST :8080/api/v1/notifications/bulk \
userIds:='["550e8400-e29b-41d4-a716-446655440001", "550e8400-e29b-41d4-a716-446655440002"]' \
channel=EMAIL \
templateName=order-confirmation \
templateVariables:='{"orderId": "BULK-001", "orderTotal": "$299.99", "userName": "Valued Customer"}' \
priority=HIGH
# Verify both users received
http :8080/api/v1/notifications/user/550e8400-e29b-41d4-a716-446655440001 size==1
http :8080/api/v1/notifications/user/550e8400-e29b-41d4-a716-446655440002 size==1Trigger: POST without content or templateName
http POST :8080/api/v1/notifications \
userId=550e8400-e29b-41d4-a716-446655440001 \
channel=EMAILResponse:
{
"success": false,
"message": "Either templateName or content must be provided",
"timestamp": "2026-01-10T10:30:00.000000+05:30"
}Trigger: Invalid userId
http POST :8080/api/v1/notifications \
userId=invalid-user-id \
channel=EMAIL \
content="Test"Response:
{
"success": false,
"message": "User not found with id: invalid-user-id",
"timestamp": "2026-01-10T10:30:00.000000+05:30"
}http POST :8080/api/v1/notifications \
userId=550e8400-e29b-41d4-a716-446655440001 \
channel=EMAIL \
templateName=non-existent-templateResponse:
{
"success": false,
"message": "Template not found: non-existent-template",
"timestamp": "2026-01-10T10:30:00.000000+05:30"
}Trigger: > 10 requests per minute per user per channel
Response:
{
"success": false,
"message": "Rate limit exceeded. Please try again later.",
"timestamp": "2026-01-10T10:30:00.000000+05:30"
}Recovery:
- Wait 60 seconds for window to reset
- Use different userId
- Use different channel
http :8080/api/v1/notifications/00000000-0000-0000-0000-000000000000Response:
{
"success": false,
"message": "Notification not found with id: 00000000-0000-0000-0000-000000000000",
"timestamp": "2026-01-10T10:30:00.000000+05:30"
}Symptom:
http: error: ConnectionError: HTTPConnectionPool: Max retries exceeded
Solutions:
- Verify application is running:
ps aux | grep spring-boot - Start application:
mvn spring-boot:run - Check port availability:
lsof -i :8080
Symptom: Actuator health returns DOWN
Diagnosis:
http :8080/actuator/health | jq '.components | to_entries[] | select(.value.status == "DOWN")'Solutions by Component:
PostgreSQL DOWN:
docker logs notification-postgres
docker restart notification-postgresRedis DOWN:
docker exec notification-redis redis-cli ping
docker restart notification-redisKafka DOWN:
docker logs notification-kafka
docker restart notification-kafkaSymptom: Warning in application logs during startup
Solution: This is normal. Kafka auto-creates topics on first message. Wait a few seconds.
# View all topics
docker exec notification-kafka-1 kafka-topics \
--bootstrap-server localhost:9092,localhost:9093,localhost:9094 \
--list
# Check topic details
docker exec notification-kafka-1 kafka-topics \
--bootstrap-server localhost:9092,localhost:9093,localhost:9094 \
--describe \
--topic notifications.email# Connect to PostgreSQL
docker exec -it notification-postgres psql -U notification_user -d notification_db
# View recent notifications
SELECT id, channel, status, created_at
FROM notifications
ORDER BY created_at DESC
LIMIT 10;
# View templates
SELECT name, channel, is_active FROM notification_templates;
# View users
SELECT id, email, phone FROM users;
# Exit
\q# Connect to Redis
docker exec -it notification-redis redis-cli
# View all rate limit keys
KEYS rate_limit:*
# Check specific user's rate limit
GET rate_limit:550e8400-e29b-41d4-a716-446655440001:EMAIL
# Clear all rate limits (for testing)
FLUSHALL
# Exit
exitSet up shell variables for easier testing:
# User IDs
export USER_1="550e8400-e29b-41d4-a716-446655440001"
export USER_2="550e8400-e29b-41d4-a716-446655440002"
# Template IDs
export TEMPLATE_WELCOME="660e8400-e29b-41d4-a716-446655440001"
export TEMPLATE_ORDER="660e8400-e29b-41d4-a716-446655440002"
export TEMPLATE_OTP="660e8400-e29b-41d4-a716-446655440003"
export TEMPLATE_SHIPPED="660e8400-e29b-41d4-a716-446655440004"
# Base URL
export API_URL="http://localhost:8080"
# Now use in commands:
http POST $API_URL/api/v1/notifications \
userId=$USER_1 \
channel=EMAIL \
templateName=welcome-email \
templateVariables:='{"userName": "John Doe"}' \
priority=HIGH| Action | HTTPie Command |
|---|---|
| Health check | http :8080/api/v1/health |
| Detailed health | http :8080/actuator/health |
| List templates | http :8080/api/v1/templates |
| Send email | http POST :8080/api/v1/notifications userId=... channel=EMAIL templateName=... templateVariables:='{...}' |
| Send SMS | http POST :8080/api/v1/notifications userId=... channel=SMS content="..." |
| Send push | http POST :8080/api/v1/notifications userId=... channel=PUSH subject="..." content="..." |
| Send in-app | http POST :8080/api/v1/notifications userId=... channel=IN_APP subject="..." content="..." |
| Bulk send | http POST :8080/api/v1/notifications/bulk userIds:='[...]' channel=... content="..." |
| Get notification | http :8080/api/v1/notifications/{id} |
| User notifications | http :8080/api/v1/notifications/user/{userId} page==0 size==10 |
| Create template | http POST :8080/api/v1/templates name=... channel=... bodyTemplate="..." |
| Update template | http PUT :8080/api/v1/templates/{id} name=... channel=... bodyTemplate="..." |
| Delete template | http DELETE :8080/api/v1/templates/{id} |
The notification system implements event-level deduplication to prevent duplicate notifications for the same event. This feature uses Redis to track event IDs with configurable TTL (Time-To-Live).
Default Settings:
- TTL: 24 hours (86400 seconds)
- Redis Key Pattern:
dedupe:event:{eventId} - Behavior: Same eventId within TTL window returns FAILED status
Configuration Location: application.yml
app:
deduplication:
ttl-seconds: 86400 # 24 hours
enabled: true# First request - should succeed
http POST :8080/api/v1/notifications \
userId=550e8400-e29b-41d4-a716-446655440001 \
channel=EMAIL \
templateName=welcome-email \
templateVariables:='{"userName": "John Doe"}' \
eventId=user-registration-12345 \
priority=HIGHExpected Response (200 OK):
{
"success": true,
"message": "Notification queued successfully",
"data": {
"id": "c57aaec7-80a4-4948-84b8-6d9582737410",
"status": "PENDING"
}
}# Second request with same eventId - should fail
http POST :8080/api/v1/notifications \
userId=550e8400-e29b-41d4-a716-446655440001 \
channel=EMAIL \
templateName=welcome-email \
templateVariables:='{"userName": "John Doe"}' \
eventId=user-registration-12345 \
priority=HIGHExpected Response (200 OK with FAILED status):
{
"success": true,
"message": "Notification failed: Duplicate event detected",
"data": {
"id": null,
"userId": "550e8400-e29b-41d4-a716-446655440001",
"channel": "EMAIL",
"priority": "HIGH",
"status": "FAILED",
"errorMessage": "Duplicate event: user-registration-12345"
}
}# Third request with different eventId - should succeed
http POST :8080/api/v1/notifications \
userId=550e8400-e29b-41d4-a716-446655440001 \
channel=EMAIL \
templateName=welcome-email \
templateVariables:='{"userName": "John Doe"}' \
eventId=user-login-67890 \
priority=HIGHExpected Response (200 OK):
{
"success": true,
"message": "Notification queued successfully",
"data": {
"id": "d67bbfc8-90b5-5959-c5c9-7e9693748521",
"status": "PENDING"
}
}# Connect to Redis
docker exec -it notification-redis redis-cli
# Check deduplication keys
KEYS dedupe:event:*
# View specific event key
GET dedupe:event:user-registration-12345
# Check TTL
TTL dedupe:event:user-registration-12345
# Expected: Returns remaining seconds until expiration# Send bulk notification with eventId
http POST :8080/api/v1/notifications/bulk \
userIds:='["550e8400-e29b-41d4-a716-446655440001", "550e8400-e29b-41d4-a716-446655440002"]' \
channel=EMAIL \
templateName=order-confirmation \
templateVariables:='{"orderId": "ORD-99999"}' \
eventId=bulk-order-confirmation-99999 \
priority=HIGHExpected Response:
{
"success": true,
"message": "Bulk notifications queued",
"data": {
"totalRequested": 2,
"successCount": 2,
"failedCount": 0,
"notificationIds": ["uuid-1", "uuid-2"]
}
}Retry same bulk request (should fail):
{
"success": true,
"message": "Bulk notifications queued",
"data": {
"totalRequested": 2,
"successCount": 0,
"failedCount": 2,
"notificationIds": [],
"failedUserIds": ["550e8400-e29b-41d4-a716-446655440001", "550e8400-e29b-41d4-a716-446655440002"]
}
}-
Use Descriptive eventIds: Include context like
user-{userId}-registrationororder-{orderId}-confirmation -
TTL Considerations:
- Short TTL (minutes) for OTP codes
- Medium TTL (hours) for user actions
- Long TTL (days) for business events
-
Idempotent Operations: Use eventId for retry-safe operations
-
Testing: Always test deduplication behavior in staging environments
-
Monitoring: Monitor Redis memory usage and deduplication hit rates
| Resource | URL |
|---|---|
| Swagger UI | http://localhost:8080/swagger-ui.html |
| Kafka UI | http://localhost:8090 |
| Actuator | http://localhost:8080/actuator |
| OpenAPI Spec | http://localhost:8080/v3/api-docs |
| HTTPie Documentation | https://httpie.io/docs |
Happy Testing with HTTPie! 🚀