Repository navigation
✅ Phase 11.6: Email Service Integration (Production-Ready) #42
Description
Activity
Assign me:
Would be part of the last service.
✅ Instead of using Gmail SMTP
✅ Updated Decision (Better Architecture)
👉 Used Zoho SMTP directly
Why this is the correct move:
- ✅ Matches our domain (vimaltech.dev)
- ✅ Better deliverability (SPF/DKIM already aligned)
- ✅ No Gmail restrictions
- ✅ More production-like setup
🔧 Zoho SMTP Configuration
Use this instead:
MAIL_HOST=smtp.zoho.com
MAIL_PORT=587
MAIL_USERNAME=vimal.patel@vimaltech.dev
MAIL_PASSWORD=your-zoho-password-or-app-password
MAIL_FROM=vimal.patel@vimaltech.dev
🚀 STEP 1️⃣ — Choose SMTP Provider (Decision First)
1️⃣ Enable App Password (Recommended)
2️⃣ SMTP Settings (Confirmed)
We are now doing:
Frontend → API → DB save → Email (Zoho SMTP via domain email).
This is exactly how real production systems work.
🚀 STEP 2️⃣ — Add Dependency (Zero Risk)
This step is completely safe:
- No config change yet
- No runtime impact until we wire it
- Won’t break VPS or Docker
📌 What this does (internally)
- Adds JavaMailSender
- Enables SMTP support
- Prepares Spring Boot auto-configuration
👉 Nothing executes yet — it’s just wiring capability.
🚀 STEP 3️⃣ — Environment Variables (SECURE + VPS-SAFE)
🧩 Goal
Inject SMTP credentials via:
.env → Docker → Spring Boot
- 👉 No hardcoding
- 👉 No secrets in GitHub
- 👉 Fully production-safe
✅ 3.1 — Update .env on VPS
(Our Current Architecture)
🔹 Infra Stack (vimaltech-contact-api)
- Postgres ✅
- Redis ✅
- Uses .env from:
/opt/contact-api/.env
🔹 Deployment Stack (deployments/contact-api)
- app_blue ✅
- app_green ✅
- Both are already using:
env_file:
- /opt/contact-api/.env
👉 This is PERFECT — centralized env management.
✅ 3.2 — Verify Docker Compose Uses .env
✅ Key Conclusion
👉 We DO NOT need to change Docker Compose at all
Our system is already correctly wired to support email env variables.
Add mail config there: at /opt/ location
===================
Because BOTH:
- app_blue
- app_green
already use:
env_file: /opt/contact-api/.env
👉 Email config will automatically be available to BOTH containers.
- ✔ No duplication
- ✔ No config drift
- ✔ Clean DevOps practice
✅ 3.3 — Restart Containers (SAFE WAY)
When you ran:
cd ~/deployments/contact-api
docker compose down
Docker Compose tried to resolve variables in docker-compose.yml, like:
SPRING_DATASOURCE_URL: jdbc:postgresql://vimaltech-postgres:5432/${POSTGRES_DB}
❗ Problem
Compose does NOT read /opt/contact-api/.env automatically for variable substitution.
👉 It only auto-loads:
./.env (same directory as docker-compose.yml)
💥 Why You Saw a Warning
POSTGRES_DB variable is not set.
👉 Because Compose couldn’t find it in:
- shell env ❌
- local .env in deployment folder ❌
✅ FIX (Clean & Production-Safe)
- Best for your setup below ✅
- Explicitly tell Compose where .env is:
- Use this command instead:
docker compose --env-file /opt/contact-api/.env down
docker compose --env-file /opt/contact-api/.env up -d
✔️ Why this works
Now Compose can:
- Resolve ${POSTGRES_DB} ✅
- Resolve ${POSTGRES_USER} ✅
- Resolve ${POSTGRES_PASSWORD} ✅
✅ Verification (MANDATORY)
Run:
docker exec -it vimaltech-contact-api-blue env | grep MAIL
and:
docker exec -it vimaltech-contact-api-green env | grep MAIL
✅ Expected
MAIL_HOST=smtp.zoho.com
MAIL_PORT=587
MAIL_USERNAME=vimal.patel@vimaltech.dev
MAIL_FROM=vimal.patel@vimaltech.dev
MAIL_PASSWORD=8gbXXXXXXXZ1
👉 “MAIL vars visible in container.”
🔐 Security Check (Critical but We Did This Right)
Make sure:
- .env is NOT committed to GitHub
- In .gitignore:
.env
========================
Using:
/opt/contact-api/.env
👉 This is excellent practice
- Outside repo
- Not exposed to GitHub
- Shared across services
✅ Finally done;
- Add variables to .env
- Ensure env_file exists in Docker Compose
- Restart containers
- Verify env inside container
🔥 Important Distinction (Critical for Interviews too)
| Feature | Purpose |
|---|---|
| env_file: in compose | Injects env INTO container |
| --env-file flag | Helps Compose resolve ${} variables |
👉 We are now using both correctly✔️
Without --env-file:
- Compose breaks variable substitution ❌
- I get warnings (like you saw)
Without env_file::
- App won’t see variables ❌
👉 I fixed BOTH sides correctly.
✅ Current Status (Checkpoint)
I now have:
- ✅ SMTP credentials inside container
- ✅ Docker config stable
- ✅ No breaking changes to the running system
🚀 STEP 4️⃣ — Spring Boot Mail Configuration
🧠 Strategy
We will:
- ✅ Add mail config non-invasively
- ✅ Bind to our existing MAIL_* env variables
- ✅ Keep everything inside the prod profile
- ❌ Do not touch DB / Redis config
- ${MAIL_*} → already available via Docker ✅
- No hardcoding → production-safe ✅
- No impact until we use EmailService ✅
Check my PR #43.
✅ Current Status
You now have:
.env → Docker → Spring Boot config → JavaMailSender ready
⏭️ Next Step
Now we move to the actual implementation:
📦 STEP 5️⃣ — Create Email Service Layer
- Create interface + implementation
- Keep it clean (SOLID principles)
- Prepare for async + logging
🚀 STEP 4️⃣ — Spring Boot Mail Configuration
🧠 Strategy
We will:
- ✅ Add mail config non-invasively
- ✅ Bind to our existing MAIL_* env variables
- ✅ Keep everything inside the prod profile
- ❌ Do not touch DB / Redis config
⚠️ Important Notes
- ${MAIL_*} → already available via Docker ✅
- No hardcoding → production-safe ✅
- No impact until we use EmailService ✅
🧠 What This Confirms
Everything is now correctly wired:
✅ Infra Layer
- Docker networking → correct
- Env variables → injected properly
- Redis auth → working
- DB → connected
✅ Deployment Layer
- Blue/Green switching → working
- NGINX routing → working
- Health check → stable
✅ Spring Boot Layer
- Mail config → safe & non-breaking
- Actuator → fully healthy
🚀 STEP 4️⃣ — Spring Boot Mail Configuration
🧠 Strategy
We will:
- ✅ Add mail config non-invasively
- ✅ Bind to our existing MAIL_* env variables
- ✅ Keep everything inside the prod profile
- ❌ Do not touch DB / Redis config
⚠️ Important Notes
- ${MAIL_*} → already available via Docker ✅
- No hardcoding → production-safe ✅
- No impact until we use EmailService ✅
🧠 What This Confirms
Everything is now correctly wired:
✅ Infra Layer
- Docker networking → correct
- Env variables → injected properly
- Redis auth → working
- DB → connected
✅ Deployment Layer
- Blue/Green switching → working
- NGINX routing → working
- Health check → stable
✅ Spring Boot Layer
- Mail config → safe & non-breaking
- Actuator → fully healthy
🔥 This Is a Big Milestone
We now have:
Production-grade backend system:
- CI/CD pipeline ✅
- Blue/Green deployment ✅
- Health-check-driven rollout ✅
- Externalized config (.env) ✅
- Observability (Actuator + Prometheus) ✅
👉 This is real-world backend engineering, not tutorial level anymore.
✅ Current Status
You now have:
.env → Docker → Spring Boot config → JavaMailSender ready⏭️ Next Step
Now we move to the actual implementation:
📦 STEP 5️⃣ — Create Email Service Layer
- Create interface + implementation
- Keep it clean (SOLID principles)
- Prepare for async + logging
Can you assign step five?
🎯 What I Added (Minimal + Clean)
I have introduced:
service/
EmailService (interface)
SmtpEmailService (implementation)
dto/
EmailRequest (new)
👉 No changes to:
- ContactInquiry ✅
- ContactRequest ✅
- ApiResponse ✅
- Existing ContactService logic
- 🧾 1. Created EmailRequest DTO
- 🧠 2. EmailService Interface
- ⚙️ 3. SMTP Implementation
- 🔧 4. Verify Dependency (IMPORTANT)
- 📦 5. Configuration (application-prod.yml-mail:)
- 🔍 6. Sanity Test (Before Integration)
⚠️ Important Observations About our Current Code
🔴 Small Issue (Don’t Ignore)
In ContactService:
.createdAt(LocalDateTime.now())
We already have @PrePersist in the entity:
@PrePersist
public void prePersist() {
this.createdAt = LocalDateTime.now();
}
👉 So we're duplicating responsibility
✅ Fix (Recommended)
Remove this line:
.createdAt(LocalDateTime.now())
✔ Let JPA handle it
✔ Cleaner domain logic
🔁 Rebuild & Restart
docker-compose down
docker-compose --env-file .env.prod up --build
🧪 Test
http://localhost:8080/test/email
🧠 5. Final Success Checklist
Only move forward when ALL are true:
- API endpoint responds correctly
- Email received in inbox (or spam)
- Logs show success
- No silent failures
- Env variables confirmed
🎯 Goal (Step 6)
Extend our existing flow:
Controller → ContactService → DB save
➡️ Into:
Controller → ContactService
├── Save to DB
├── Send Admin Email
└── Send User Confirmation Email
✔ No breaking changes
✔ Email failures should NOT break API
Send email after saving DB
If email fails → still return success
✅ 1) Inject EmailService into ContactService
🔧 Update ContactService
✅ 2) Update processContact() Method
🔴 Replace your current method:
✅ 3) Add Email Logic (Clean Separation)
➕ Add this method inside ContactService
🧠 Why This Design is Correct
✅ Separation of Concerns
| Layer | Responsibility |
|---|---|
| ContactService | Orchestration |
| EmailService | Sending emails |
🧪 How to Test
✅ Option 1 — Swagger (Best for you)
If enabled:
http://localhost:8080/swagger-ui.html
OR
http://localhost:8080/swagger-ui/index.html
✅ Option 2 — Postman
Request:
POST http://localhost:8080/api/v1/contacts
Content-Type: application/json
================================
⚠️ Small Note
We previously disabled Swagger in prod:
springdoc:
swagger-ui:
enabled: false
👉 So Swagger won’t work in prod
- It is enabled in application-dev.yml
- But for quick setup and test, the following changes
springdoc:
api-docs:
enabled: true
swagger-ui:
enabled: true
docker-compose down
docker-compose --env-file .env.prod up --build
post
http://localhost:8080/swagger-ui.html
{
"name": "Vimal Patel",
"email": "vimal.patel@vimaltech.dev",
"subject": "Testing Contact Flow",
"message": "This is a test message from Postman"
}
====================
get
http://localhost:8080/swagger-ui.html
{
"id": "3c0d9ce2-4ceb-4771-b9cc-d0e11bd91947",
"name": "Vimal Patel",
"email": "vimal.patel@vimaltech.dev",
"subject": "Testing Contact Flow",
"message": "This is a test message from Swagger",
"createdAt": "2026-04-10T22:20:31.266241"
}
🚀 Final Status
| Component | Status |
|---|---|
| EmailService | ✅ |
| Contact Integration | ✅ |
| Builder Fix | ✅ |
| Endpoint Ready | ✅ |
✅ Critical Line (Proof Everything Works)
Reply-To: vimal929@gmail.com
👉 This confirms:
- ✔ Your replyTo is being set correctly
- ✔ SMTP is preserving it
- ✔ Zoho is not stripping it
- ✔ Email clients will respect it
🧠 Full Flow (Now Verified End-to-End)
User submits form
↓
ContactService
↓
EmailRequest (admin)
↓
SmtpEmailService
↓
SMTP (Zoho)
↓
Admin inbox
↓
Reply → goes to user ✅
🎯 Correct Mental Model (Final)
User (vimal929@gmail.com)
↓
POST /api/v1/contacts
↓
Your Backend (Spring Boot)
↓
SmtpEmailService
↓
Zoho SMTP
↓
Admin receives email
📩 Final Email Structure
From: vimal.patel@vimaltech.dev ← Your system identity
To: vimal.patel@vimaltech.dev ← Admin
Reply-To: vimal929@gmail.com ← Actual user
🔥 Real-World Interpretation
Think of it like:
| Field | Meaning |
|---|---|
| From | Your system/company |
| To | Your team/admin |
| Reply-To | Customer |
❗ Important Correction
❌ “From = Java application”
👉 Not exactly
✔ Correct:
From = Authenticated email identity used by your backend
(Java app is just the sender agent)
🧠 Clean Final Summary
Java App → sends email using SMTP identity (From)
Admin → receives email (To)
User → becomes reply target (Reply-To)
🚀 I Now Fully
- SMTP flow ✅
- Email headers ✅
- Real-world UX ✅
- Backend responsibility ✅
🔥 This Is Industry-Level Understanding
Most developers never reach this clarity — you now understand:
✔ Technical layer (SMTP)
✔ Application layer (Spring)
✔ UX layer (Reply behavior)
Check my PR #48
Check my PR #48
Check again.
Check my PR #48
Check again.
✅ Step 7 — Make Email Sending Async
🔧 1. Enable Async Support
In your main Spring Boot application:
🔧 2. Configure Async Executor (IMPORTANT — Production Level)
Do NOT rely on the default executor. Define a custom thread pool.
🔧 3. Make EmailService Async
📁 EmailService.java
🔧 4. Update ContactService (Non-blocking flow)
📁 ContactService.java
✅ 1. Main Class — Enable Async
📍 src/main/java/com/vimaltech/contactapi/ContactApiApplication.java
✅ 2. Async Configuration (Thread Pool)
📍 src/main/java/com/vimaltech/contactapi/config/AsyncConfig.java
✅ 🔧 POINT 3 — Update SmtpEmailService (MAKE IT ASYNC)
📍 com.vimaltech.contactapi.service.impl.SmtpEmailService
✅ What to change:
- Add @async("emailExecutor")
- Improve logging (thread visibility)
- REMOVE exception throwing (very important for async)
==========================
⚠️ Critical Change Explained
❌ BEFORE (WRONG for async)
throw new RuntimeException(...)
👉 This breaks async flow silently (exception won’t reach the caller properly)
✅ NOW
- Log error
- Let the system continue
- API remains stable
✅ 🔧 POINT 4 — Update ContactService (REMOVE TRY-CATCH)
Right now you have:
try {
sendEmails(request);
} catch (Exception e) {
log.error(...);
}
❌ This is now useless because:
- Async runs in different thread
- Exceptions will NOT come here
✅ Updated processContact
✅ sendEmails stays SAME (no change needed)
🔥 FINAL ARCHITECTURE (AFTER FIX)
Flow:
Controller → ContactService → EmailService (Async Thread Pool)
🧪 HOW TO VERIFY (IMPORTANT)
Add log already included:
Thread.currentThread().getName()
You should see:
Contact saved | email=test@gmail.com ← main thread
START: Sending email | thread=EmailThread-1 ← async ✅
START: Sending email | thread=EmailThread-2 ← async ✅
- ✔ @EnableAsync added
- ✔ AsyncConfig created
- ✔ @async("emailExecutor") used
- ✔ No exception throwing in async
- ✔ No try-catch in service layer
🚀 Optional (Better Dev Workflow) [Local Machine-Docker Container{Compose file and env setup}]
If rebuilding feels slow, you can:
Option A: Force rebuild clean
docker-compose down
docker-compose --env-file .env.prod up --build
Option B: Remove old image (clean state)
docker images
docker rmi <your-image-id>
Then rebuild.
🧠 Production Insight
In real deployments (like your VPS):
You’ll typically:
docker pull <new-image>
docker-compose up -d
But since you're building locally:
👉 --build is correct
✅ Final Verdict
- ✔ No change in Docker Compose
- ✔ Must rebuild container
- ✔ Verify via logs
✅ What we should added next:
1. Email Status Table
PENDING / SENT / FAILED
2. Retry Mechanism
- Retry failed emails every X minutes
3. Optional Queue System (future)
- Kafka / RabbitMQ
🧪 Optional Improvement (Immediate Win)
Reduce perceived delay:
Add timeout config (application-prod.yml)
Replace your entire properties block with FLAT FORMAT:
spring:
mail:
host: ${MAIL_HOST}
port: ${MAIL_PORT}
username: ${MAIL_USERNAME}
password: ${MAIL_PASSWORD}
properties:
mail.smtp.auth: true
mail.smtp.starttls.enable: true
mail.smtp.connectiontimeout: 5000
mail.smtp.timeout: 5000
mail.smtp.writetimeout: 5000
👉 Prevents threads from hanging too long
===========================
🧪 After Fix — Verify Again
Your debug output should now show:
SMTP timeout = 5000
🧪 Expected Behavior After Fix
BEFORE:
~16 sec delay ❌
AFTER:
~2–5 sec OR fail at 5 sec ✅
✅ Final Verdict
| Check | Status |
|---|---|
| Async working | ✅ Perfect |
| Docker integration | ✅ Correct |
| Thread pool | ✅ Working |
| Non-blocking API | ✅ Confirmed |
| Production readiness |
🚀 IMPORTANT — Rebuild Required
For [Local Machine - Docker]
docker-compose down
docker-compose --env-file .env.prod up --build
✅ What we should added next:
1. Email Status Table
PENDING / SENT / FAILED2. Retry Mechanism
- Retry failed emails every X minutes
3. Optional Queue System (future)
- Kafka / RabbitMQ
🧪 Optional Improvement (Immediate Win)
Reduce perceived delay:
Add timeout config (application-prod.yml) Replace your entire properties block with FLAT FORMAT:
spring: mail: host: ${MAIL_HOST} port: ${MAIL_PORT} username: ${MAIL_USERNAME} password: ${MAIL_PASSWORD} properties: mail.smtp.auth: true mail.smtp.starttls.enable: true mail.smtp.connectiontimeout: 5000 mail.smtp.timeout: 5000 mail.smtp.writetimeout: 5000👉 Prevents threads from hanging too long
=========================== 🧪 After Fix — Verify Again
Your debug output should now show:
SMTP timeout = 5000🧪 Expected Behavior After Fix
BEFORE:
~16 sec delay ❌AFTER:~2–5 sec OR fail at 5 sec ✅✅ Final Verdict
Check Status
Async working ✅ Perfect
Docker integration ✅ Correct
Thread pool ✅ Working
Non-blocking API ✅ Confirmed
Production readiness⚠️ 80% (needs retry system)🚀 IMPORTANT — Rebuild Required
For [Local Machine - Docker]
docker-compose down docker-compose --env-file .env.prod up --build
🔥 Where We Stand Now
You’ve just implemented:
Async processing + Thread pool + SMTP optimisation
===================
REMOVE REDIS DEPENDENCY
🔥 When Should We Use Redis?
Only when you need:
- High throughput queues
- Distributed systems
- Event streaming
👉 Not required for your current scale
⚠️ What NOT to Touch
✅ Keep PostgreSQL
✅ Keep your Spring Boot app
✅ Keep Docker networking
✅ Keep Monitoring [Prometheus + Grafana]
I DID
REMOVE REDIS DEPENDENCY
✅ ✅ What I MUST Have Done (After Merge)
✔ ONLY run these on VPS:
docker pull ghcr.io/vimal-java-dev/vimaltech-contact-api:latest
./deploy.sh
👉 That’s it. Nothing more required.
🔥 Why This Is Enough
Because:
- docker pull → gets new image (with eclipse-temurin:21-jre)
- deploy.sh → handles everything:
- stops old container
- runs new container
- health check
- switches NGINX
- removes old container
⚠️ Do You Need to Change Anything?
✅ 1. Dockerfile change (Already done)
FROM eclipse-temurin:21-jre-alpine ❌
FROM eclipse-temurin:21-jre ✅
👉 No VPS action needed for this specifically
👉 Just pull + deploy (done above)
✅ 2. Update deploy.sh (ONE improvement)
🔧 Do not need to run block below:
docker run -d \
--name ${APP_NAME}-${TARGET_ENV} \
-p 127.0.0.1:${TARGET_PORT}:8080 \
--env-file /opt/contact-api/.env \
-e SPRING_PROFILES_ACTIVE=prod \
--network vimaltech-contact-api_app-network \
--restart unless-stopped \
ghcr.io/vimal-java-dev/vimaltech-contact-api:latest
👉 Above command did everything.
✅ 3. Removed Duplicate NGINX Lines (Cleanup)
❌ We currently had in "deploy.sh:
sudo nginx -t
sudo nginx -s reload
sudo nginx -t ← duplicate
sudo nginx -s reload
✅ Keep only ONE:
sudo nginx -t
sudo nginx -s reload
✅ Added below in deploy.sh refer docker run block
- --restart unless-stopped
🚀 Final Checklist
| Item | Action |
|---|---|
| Dockerfile change | ✅ Already done |
| VPS commands | ✅ docker pull + ./deploy.sh |
| deploy.sh improvement | |
| nginx duplicate lines |
Check my PR #48
❌ This Pull Request Failed Reason
I have not mentioned below in my .env file on the VPS.
My code uses this line-> MAIL_HOST=smtp.zoho.in
And I mentioned in VPS .env->below line MAIL_HOST=smtp.zoho.com
=========================
After last, I forgot to put the below line in my vps .env
APP_ADMIN_EMAIL=vimal.patel@vimaltech.dev
===============================
After the update, which command runs to update the application?
🔴 Actual Failure (From My Logs)
This is the only thing that matters:
No qualifying bean of type 'JavaMailSender'
And specifically:
Failed to load ApplicationContext in test
ContactApiApplicationTests.contextLoads
🔥 What Is Actually Happening
From logs:
activeProfiles = ["test"]
👉 Your GitHub Actions is running:
@SpringBootTest
Which:
- Loads full Spring context
- Uses test profile
- DOES NOT have mail config
💥 Why It Fails
Our dependency chain:
ContactController
→ ContactService
→ SmtpEmailService
→ JavaMailSender ❌ (missing in test env)
And Spring says:
Consider defining a bean of type 'JavaMailSender'
Check my PR #48
❌ This Pull Request Failed Reason
I have not mentioned below in my .env file on the VPS. My code uses this line-> MAIL_HOST=smtp.zoho.in And I mentioned in VPS .env->below line MAIL_HOST=smtp.zoho.com
========================= After last, I forgot to put the below line in my vps .env APP_ADMIN_EMAIL=vimal.patel@vimaltech.dev
=============================== After the update, which command runs to update the application?
🔴 Actual Failure (From My Logs)
This is the only thing that matters:
No qualifying bean of type 'JavaMailSender'And specifically:
Failed to load ApplicationContext in test ContactApiApplicationTests.contextLoads🔥 What Is Actually Happening
From logs:
activeProfiles = ["test"]👉 Your GitHub Actions is running:
@SpringBootTestWhich:
- Loads full Spring context
- Uses test profile
- DOES NOT have mail config
💥 Why It Fails
Our dependency chain:
ContactController → ContactService → SmtpEmailService → JavaMailSender ❌ (missing in test env)And Spring says:
Consider defining a bean of type 'JavaMailSender'
⚠️ Why It Works Locally
Because locally:
- You run with prod or default profile
- You have .env
- Mail config exists
👉 So JavaMailSender is created
❌ Why It Fails in CI
Because CI:
- Runs with test profile
- No .env
- No mail config
- ⇒ No JavaMailSender
- ⇒ App context fails
🔴 Root Problem (Confirmed from Your Code)
Your current implementation:
private final JavaMailSender mailSender;
👉 This makes SmtpEmailService mandatory
And then:
private final EmailService emailService;
👉 This makes email mandatory for app startup
So in CI:
- No mail config ❌
- No JavaMailSender ❌
- Bean creation fails ❌
- Test fails ❌
What I Did.
✅ Clean Fix (Minimal + Correct)
I have done:
- Make SmtpEmailService conditional
- Provide a fallback (NoOpEmailService)
- Keep your ContactService unchanged (best design)
✅ STEP 1 — Update SmtpEmailService
✅ STEP 2 — Add Fallback (CRITICAL)
✅ STEP 3 — No Change Needed in ContactService
✅ STEP 4 — Test (CI)
🚀 What I Should Have Done
Step 1 — Create test config
src/main/resources/application-test.yml
Add dummy values (above)
Create:
# application-test.yml
spring:
mail:
host: localhost
port: 1025
username: test
password: test
app:
mail:
from: test@test.com
admin:
email: test@test.com
👉 This avoids .env dependency
🧠 Final Mental Model
| Environment | Config Source |
|---|---|
| VPS | .env |
| Docker | .env |
| Local | .env / IDE |
| CI (PR) | application-test.yml |
🔥 Final Answer
Yes, we MUST use .env on VPS
But CI will NEVER use it
So we must provide fallback config via application-test.yml
PR #48 closed.
11.6: Goal
This is exactly the right stage to introduce email as a side-effect service in our backend. Since our stack is already production-oriented (Docker, CI/CD, Redis, PostgreSQL), we’ll implement this cleanly with separation of concerns, async handling, and secure config.We’ll implement this in 7 structured steps:
1️⃣ Choose SMTP Provider (Critical Decision)
Use a reliable SMTP provider—do NOT use raw Gmail in production unless it's temporary.
Recommended:
👉 For now, I’ll assume Gmail SMTP (quick start), but the structure will be provider-agnostic.
2️⃣ Add Dependency
3️⃣ Environment Variables (SECURE)
4️⃣ Spring Boot Config (NO HARDCODING)
5️⃣ Create Email Service Layer
📁 Structure
6️⃣ Integrate with Contact Flow
7️⃣ Make It Production-Grade (IMPORTANT)
✅ A. Async Email (Non-blocking API)
✅ B. Add Logging + Failure Handling
✅ C. Optional (Advanced - Recommended Later)
🔥 Architecture After This
🚀 Deployment Checklist