Skip to content

Commit d97db18

Browse files
Phase 11.6: Async Email Integration with Thread Pool & SMTP Optimization (#48)
* feat: implement async email sending with thread pool and SMTP timeout optimization chore: remove unused Redis configuration and dependencies Signed-off-by: vimal-tech-starter <varnam2311@gmail.com> * chore: rerun CI Signed-off-by: vimal-tech-starter <varnam2311@gmail.com> * fix: add fallback email service for CI Signed-off-by: vimal-tech-starter <varnam2311@gmail.com> --------- Signed-off-by: vimal-tech-starter <varnam2311@gmail.com>
1 parent c2be91f commit d97db18

File tree

14 files changed

+262
-31
lines changed

14 files changed

+262
-31
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ COPY src ./src
1010
RUN mvn clean package -DskipTests
1111

1212
# ---------- Stage 2: Runtime ----------
13-
FROM eclipse-temurin:21-jre-alpine
13+
FROM eclipse-temurin:21-jre
1414

1515
WORKDIR /app
1616

pom.xml

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -99,18 +99,6 @@
9999
<version>8.9.0</version>
100100
</dependency>
101101

102-
<dependency>
103-
<groupId>com.bucket4j</groupId>
104-
<artifactId>bucket4j-redis</artifactId>
105-
<version>8.9.0</version>
106-
</dependency>
107-
108-
<!-- Spring Redis -->
109-
<dependency>
110-
<groupId>org.springframework.boot</groupId>
111-
<artifactId>spring-boot-starter-data-redis</artifactId>
112-
</dependency>
113-
114102
<dependency>
115103
<groupId>org.springframework.boot</groupId>
116104
<artifactId>spring-boot-starter-actuator</artifactId>

src/main/java/com/vimaltech/contactapi/ContactApiApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.scheduling.annotation.EnableAsync;
56

67
@SpringBootApplication
8+
@EnableAsync
79
public class ContactApiApplication {
810

911
public static void main(String[] args) {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.vimaltech.contactapi.config;
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties;
4+
import org.springframework.stereotype.Component;
5+
6+
@ConfigurationProperties(prefix = "app.admin")
7+
@Component
8+
public class AppProperties {
9+
10+
private String email;
11+
12+
public String getEmail() {
13+
return email;
14+
}
15+
16+
public void setEmail(String email) {
17+
this.email = email;
18+
}
19+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.vimaltech.contactapi.config;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
6+
7+
import java.util.concurrent.Executor;
8+
9+
@Configuration
10+
public class AsyncConfig {
11+
12+
@Bean(name = "emailExecutor")
13+
public Executor emailExecutor() {
14+
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
15+
executor.setCorePoolSize(3); // baseline threads (good for VPS)
16+
executor.setMaxPoolSize(8); // peak load
17+
executor.setQueueCapacity(100); // queue size
18+
executor.setThreadNamePrefix("EmailThread-");
19+
executor.initialize();
20+
return executor;
21+
}
22+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.vimaltech.contactapi.controller;
2+
3+
import com.vimaltech.contactapi.config.AppProperties;
4+
import com.vimaltech.contactapi.dto.EmailRequest;
5+
import com.vimaltech.contactapi.service.EmailService;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.context.annotation.Profile;
8+
import org.springframework.web.bind.annotation.GetMapping;
9+
import org.springframework.web.bind.annotation.RequestMapping;
10+
import org.springframework.web.bind.annotation.RestController;
11+
12+
//@Profile({"dev"})
13+
//@Profile({"dev", "local"})
14+
@RestController
15+
@RequestMapping("/test")
16+
@RequiredArgsConstructor
17+
public class EmailTestController {
18+
19+
private final EmailService emailService;
20+
private final AppProperties appProperties;
21+
22+
@GetMapping("/email")
23+
public String testEmail() {
24+
25+
emailService.sendEmail(
26+
EmailRequest.builder()
27+
.to(appProperties.getEmail()) // 👈 CHANGE THIS
28+
.subject("Test Email from VimalTech API")
29+
.body("Email service is working successfully 🚀")
30+
.build()
31+
);
32+
33+
return "Email sent successfully";
34+
}
35+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.vimaltech.contactapi.dto;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@Builder
8+
public class EmailRequest {
9+
private final String to;
10+
private final String subject;
11+
private final String body;
12+
13+
private final String replyTo;
14+
}

src/main/java/com/vimaltech/contactapi/service/ContactService.java

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,91 @@
11
package com.vimaltech.contactapi.service;
22

3+
import com.vimaltech.contactapi.config.AppProperties;
34
import com.vimaltech.contactapi.dto.ContactRequest;
5+
import com.vimaltech.contactapi.dto.EmailRequest;
46
import com.vimaltech.contactapi.entity.ContactInquiry;
57
import com.vimaltech.contactapi.repository.ContactRepository;
6-
import lombok.RequiredArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
79
import org.springframework.stereotype.Service;
810

9-
import java.time.LocalDateTime;
1011
import java.util.List;
1112

1213
@Service
13-
@RequiredArgsConstructor
14+
@Slf4j
1415
public class ContactService {
1516

1617
private final ContactRepository contactRepository;
18+
private final EmailService emailService;
19+
private final AppProperties appProperties;
20+
21+
public ContactService(ContactRepository repo, EmailService emailService, AppProperties appProperties) {
22+
this.contactRepository = repo;
23+
this.emailService = emailService;
24+
this.appProperties = appProperties;
25+
}
1726

1827
public void processContact(ContactRequest request) {
1928

29+
// ✅ 1. Save to DB (unchanged behavior)
2030
ContactInquiry inquiry = ContactInquiry.builder()
2131
.name(request.name())
2232
.email(request.email())
2333
.subject(request.subject())
2434
.message(request.message())
25-
.createdAt(LocalDateTime.now())
2635
.build();
2736

2837
contactRepository.save(inquiry);
38+
39+
log.info("Contact saved | email={}", request.email());
40+
41+
// ✅ 2. Send emails {(non-blocking for business logic), try-catch removed}
42+
// ✅ Fire async emails (NO try-catch)
43+
sendEmails(request);
44+
}
45+
46+
// ✅ NEW METHOD (clean separation)
47+
private void sendEmails(ContactRequest request) {
48+
49+
// 📩 Admin Email
50+
EmailRequest adminEmail = EmailRequest.builder()
51+
.to(appProperties.getEmail())
52+
.subject("New Contact Inquiry: " +
53+
(request.subject() != null ? request.subject() : "No Subject"))
54+
.body("""
55+
New contact inquiry received:
56+
57+
Name: %s
58+
Email: %s
59+
Subject: %s
60+
61+
Message:
62+
%s
63+
""".formatted(
64+
request.name(),
65+
request.email(),
66+
request.subject(),
67+
request.message()
68+
))
69+
.replyTo(request.email())
70+
.build();
71+
72+
emailService.sendEmail(adminEmail);
73+
74+
// 📩 User Confirmation Email
75+
EmailRequest userEmail = EmailRequest.builder()
76+
.to(request.email())
77+
.subject("Thanks for contacting VimalTech")
78+
.body("""
79+
Hi %s,
80+
81+
Thank you for reaching out. We have received your message and will respond shortly.
82+
83+
Best regards,
84+
VimalTech Team
85+
""".formatted(request.name()))
86+
.build();
87+
88+
emailService.sendEmail(userEmail);
2989
}
3090

3191
public List<ContactInquiry> getAllContacts() {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.vimaltech.contactapi.service;
2+
3+
import com.vimaltech.contactapi.dto.EmailRequest;
4+
5+
public interface EmailService {
6+
void sendEmail(EmailRequest request);
7+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.vimaltech.contactapi.service.impl;
2+
3+
import com.vimaltech.contactapi.dto.EmailRequest;
4+
import com.vimaltech.contactapi.service.EmailService;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.context.annotation.Primary;
7+
import org.springframework.stereotype.Service;
8+
9+
@Service
10+
@Primary
11+
@Slf4j
12+
public class NoOpEmailService implements EmailService {
13+
14+
@Override
15+
public void sendEmail(EmailRequest request) {
16+
log.warn("Email disabled (NoOp) | to={}", request.getTo());
17+
}
18+
}

0 commit comments

Comments
 (0)