The project has a comprehensive email notification system implemented using RabbitMQ for asynchronous email delivery. The backend service publishes email messages to RabbitMQ queues, and an external email consumer service handles the actual email delivery.
The system uses RabbitMQ for asynchronous email delivery with the following components:
┌─────────────────────────────────────────────────────────────────┐
│ Email Notification Flow │
│ │
│ Business Logic (Service Layer) │
│ ↓ │
│ Create PublishEmailMessage │
│ ↓ │
│ Call rabbitMQEmailService.PublishEmailMessage() │
│ ↓ │
│ Marshal to JSON │
│ ↓ │
│ Publish to RabbitMQ (exchange + queue) │
│ ↓ │
│ External Email Consumer Service │
│ ↓ │
│ Send actual email │
└─────────────────────────────────────────────────────────────────┘
app/service/rabbit_mq/mailing/)Service Layer:
PublishEmailMessage() - Publishes email messages to RabbitMQRABBITMQ_EMAIL_EXCHANGE (mail.exchange.direct)RABBITMQ_EMAIL_QUEUE (mail_service_queue)Message Structure:
type PublishEmailMessage struct {
SendTo string // Recipient email
Type string // Email template type
Name string // Recipient name
Subject string // Email subject
Data map[string]interface{} // Template data
}
Implementation:
func (r *ServiceImpl) PublishEmailMessage(message *PublishEmailMessage) {
r.rabbitMQService.PublishMessage(
message,
os.Getenv("RABBITMQ_EMAIL_EXCHANGE"),
os.Getenv("RABBITMQ_EMAIL_QUEUE")
)
}
The system supports 11 different email types (defined in app/service/rabbit_mq/mailing/service.go):
| Email Type | Constant | Purpose |
|---|---|---|
| Company Member Invitation | DI_COMPANY_MEMBER_INVITATION |
Invite users to join a company |
| Program Member Invitation | DI_PROGRAM_MEMBER_INVITATION |
Invite users to join a program |
| Program Member Request | DI_PROGRAM_MEMBER_REQUEST |
Request confirmation to join program |
| Program Member Added | DI_PROGRAM_MEMBER_ADD |
Notify user they were added to program |
| Program Creation | DI_PROGRAM_CREATION |
Notify admins of new program creation |
| ISV Admin Request Approval | DI_ISV_REQUEST_ADMIN_APPROVAL |
Approve ISV admin request |
| ISV Admin Request Denial | DI_ISV_REQUEST_ADMIN_DENY |
Deny ISV admin request |
| Verified Professional | DI_BECOME_A_VERIFIED_PROFESSIONAL_EMAIL |
Invite to become verified professional |
| Contact Support | CONTACT_SUPPORT_EMAIL |
Contact support form submission |
| Work Email Update Code | WORK_EMAIL_UPDATE_CODE |
Verification code for work email update |
app/service/rabbit_mq/publisher.go)Singleton Pattern Implementation:
main.go with rabbit_mq.InitPublisher()rabbit_mq.ClosePublisher()Key Features:
type Publisher struct {
connection *amqp.Connection
channel *amqp.Channel
ctx context.Context
mutex sync.Mutex
ready chan struct{}
}
Publisher Methods:
InitPublisher() - Initialize singleton instanceGetPublisher() - Get singleton instancePublish(exchange, routingKey, body) - Publish message to RabbitMQClosePublisher() - Graceful shutdownAuto-Reconnection Logic: The publisher automatically reconnects to RabbitMQ if the connection is lost:
func (p *Publisher) run() {
for {
if err := p.connect(); err != nil {
log.Errorf("Publisher connection error: %v", err)
time.Sleep(2 * time.Second)
continue
}
// Wait for connection close notification
closeErr := <-p.connection.NotifyClose(make(chan *amqp.Error))
log.Warnf("Publisher connection closed: %v", closeErr)
// Reconnect after 2 seconds
time.Sleep(2 * time.Second)
}
}
Location: app/service/company/service.go:770
rabbitMessage := new(rabbitMQEmailService.PublishEmailMessage)
rabbitMessage.SendTo = invitation.Email
rabbitMessage.Name = invitation.Name
rabbitMessage.Type = rabbitMQEmailService.CompanySendInvitationEmail
rabbitMessage.Subject = rabbitMQEmailService.SendInvitationSubject
rabbitMessage.Data = map[string]interface{}{
"invitation_token": invitation.Token,
"company_name": company.Name,
}
s.rabbitMQEmailService.PublishEmailMessage(rabbitMessage)
Location: app/service/program/service.go:94
rabbitEmailMessage := rabbitMQEmailService.PublishEmailMessage{
Type: rabbitMQEmailService.ProgramCreation,
Subject: "New program created: " + programData.Name,
Data: map[string]interface{}{
"program_id": programData.ID.String(),
"program_name": programData.Name,
"company_name": programData.CompanyName,
"created_by": user.Firstname + " " + user.Lastname,
"created_at": time.Now().Format("02 Jan 2006"),
},
}
go s.rabbitMQEmailService.PublishEmailMessage(&rabbitEmailMessage)
Location: app/service/program/service.go:586
rabbitMessage := new(rabbitMQEmailService.PublishEmailMessage)
rabbitMessage.SendTo = invitation.Email
rabbitMessage.Name = invitation.Name
rabbitMessage.Type = rabbitMQEmailService.ProgramSendInvitationEmail
rabbitMessage.Subject = rabbitMQEmailService.SendInvitationSubject
rabbitMessage.Data = map[string]interface{}{
"invitation_token": invitation.Token,
"program_name": programPage.Name,
"role": strings.ToLower(invitation.Role),
}
s.rabbitMQEmailService.PublishEmailMessage(rabbitMessage)
Location: app/service/program/service.go:519
rabbitMsg := new(rabbitMQEmailService.PublishEmailMessage)
rabbitMsg.SendTo = ownerUser.Email
rabbitMsg.Name = ownerUser.Firstname + " " + ownerUser.Lastname
rabbitMsg.Type = rabbitMQEmailService.ProgramMemberAdd
rabbitMsg.Subject = fmt.Sprintf("You have been added to the %s program", programPage.Name)
rabbitMsg.Data = map[string]interface{}{
"user_name": ownerUser.Firstname + " " + ownerUser.Lastname,
"program_id": programPage.ID,
"program_name": programPage.Name,
"company_name": company.LegalName,
"admin_name": user.Firstname + " " + user.Lastname,
"role": invitation.Role,
}
go s.rabbitMQEmailService.PublishEmailMessage(rabbitMsg)
Location: app/service/user/service.go:205
rabbitMessage := new(rabbitMQEmailService.PublishEmailMessage)
rabbitMessage.SendTo = email
rabbitMessage.Name = user.Firstname
rabbitMessage.Type = rabbitMQEmailService.ISVBecomeProEmail
rabbitMessage.Subject = rabbitMQEmailService.ISVBecomeProSubject
rabbitMessage.Data = map[string]interface{}{
"token": token,
}
go s.rabbitMQEmailService.PublishEmailMessage(rabbitMessage)
Location: app/service/user/service.go:337
rabbitEmailMessage := rabbitMQEmailService.PublishEmailMessage{
SendTo: email,
Type: rabbitMQEmailService.WorkEmailUpdateCode,
Subject: "Work Email Update",
Data: map[string]interface{}{
"code": code,
"email": email,
},
}
s.rabbitMQEmailService.PublishEmailMessage(&rabbitEmailMessage)
Location: app/service/billing_center/service.go:25
rabbitMessage := new(rabbitMQEmailService.PublishEmailMessage)
rabbitMessage.SendTo = os.Getenv("CONTACT_SUPPORT_ADMIN_EMAIL")
rabbitMessage.Name = "Admins"
rabbitMessage.Type = rabbitMQEmailService.SendContactSupportEmail
rabbitMessage.Subject = rabbitMQEmailService.SendContactSupportSubject
rabbitMessage.Data = map[string]interface{}{
"first_name": input.Firstname,
"last_name": input.Lastname,
"username": input.Firstname + " " + input.Lastname,
"email": input.Email,
"topic": input.Topic,
"description": input.Description,
"attachment": input.Attachment,
}
go s.rabbitMQEmailService.PublishEmailMessage(rabbitMessage)
Location: app/service/company/service.go:920-958
rabbitMessage := new(rabbitMQEmailService.PublishEmailMessage)
rabbitMessage.SendTo = requestUser.Email
rabbitMessage.Name = requestUser.Firstname + " " + requestUser.Lastname
rabbitMessage.Data = map[string]interface{}{
"request_user_name": requestUser.Firstname,
"admin_name": user.Firstname,
}
switch action {
case constant.Deny:
invitation.Rejected = true
rabbitMessage.Type = rabbitMQEmailService.ISVRequestDeny
rabbitMessage.Subject = rabbitMQEmailService.ISVRequestDenySubject
case constant.Approve:
invitation.Approved = true
rabbitMessage.Type = rabbitMQEmailService.ISVRequestApproval
rabbitMessage.Subject = rabbitMQEmailService.ISVRequestApprovalSubject
}
s.rabbitMQEmailService.PublishEmailMessage(rabbitMessage)
The system has email action handlers for link clicks in emails:
Route Configuration: app/route/email_action/email_action.go
emailActions := api.Group("/email-actions", init.AuthMiddleware.ValidateTokenMiddleware())
emailActions.Route("/program", email_action.Program(init))
emailActions.Route("/user", email_action.User(init))
Available Email Actions:
| Route | Controller Method | Purpose |
|---|---|---|
GET /api/email-actions/program/member-join-confirm |
ProgramController.MemberJoinConfirm |
Confirm program membership from email link |
GET /api/email-actions/user/user-activation |
UserController.EmailActionActivateUser |
Activate user account from email link |
Email-related environment variables (.env.dist):
# RabbitMQ Email Configuration
RABBITMQ_EMAIL_EXCHANGE=mail.exchange.direct
RABBITMQ_EMAIL_QUEUE=mail_service_queue
# Contact Support Admin Emails (comma-separated)
CONTACT_SUPPORT_ADMIN_EMAIL=devinsider-production@wylog.com,diamondrastier@gmail.com,nralambomanana@wylog.com,asimbola@wylog.com
✅ Asynchronous Processing - All emails sent via goroutines (go s.rabbitMQEmailService.PublishEmailMessage()) to avoid blocking the main request
✅ Template-based - Uses email type constants for template selection on consumer side
✅ Resilient - Auto-reconnection to RabbitMQ on connection failure
✅ Separation of Concerns - Backend only publishes messages, external email service handles delivery
✅ Multi-recipient Support - Contact support sends to multiple admin emails
✅ Thread-safe - Publisher uses mutex for concurrent access
✅ Type-safe - Email types defined as constants to prevent typos
✅ Flexible Data - Generic map[string]interface{} for template data
┌────────────────────────────────────────────────────────────────────┐
│ Email Flow Diagram │
└────────────────────────────────────────────────────────────────────┘
┌─────────────┐
│ Service │ 1. Create email message
│ Layer │ with type, recipient, subject, data
└──────┬──────┘
│
▼
┌──────────────────────┐
│ RabbitMQ Email │ 2. Publish to RabbitMQ
│ Service │
│ PublishEmailMessage()│
└──────┬───────────────┘
│
▼
┌──────────────────────┐
│ RabbitMQ Service │ 3. Marshal to JSON
│ PublishMessage() │
└──────┬───────────────┘
│
▼
┌──────────────────────┐
│ RabbitMQ Publisher │ 4. Get singleton instance
│ GetPublisher() │ Wait for ready signal
└──────┬───────────────┘
│
▼
┌──────────────────────────────────────┐
│ RabbitMQ Server │ 5. Publish to exchange
│ Exchange: mail.exchange.direct │ Route to queue
│ Queue: mail_service_queue │
└──────┬───────────────────────────────┘
│
▼
┌──────────────────────┐
│ Email Consumer │ 6. Consume from queue
│ (External Service) │ Select template by type
│ │ Render with data
│ │ Send email via SMTP
└──────────────────────┘
Define constant in app/service/rabbit_mq/mailing/service.go:
const (
NewEmailType = "NEW_EMAIL_TYPE"
NewEmailSubject = "Email Subject"
)
Create email message in service layer:
rabbitMessage := new(rabbitMQEmailService.PublishEmailMessage)
rabbitMessage.SendTo = recipient.Email
rabbitMessage.Name = recipient.Name
rabbitMessage.Type = rabbitMQEmailService.NewEmailType
rabbitMessage.Subject = rabbitMQEmailService.NewEmailSubject
rabbitMessage.Data = map[string]interface{}{
"key1": "value1",
"key2": "value2",
}
Publish asynchronously:
go s.rabbitMQEmailService.PublishEmailMessage(rabbitMessage)
Update external email consumer to handle the new email type with corresponding template
Use goroutine (async) when:
Use synchronous when:
Most emails in the codebase use goroutines (go s.rabbitMQEmailService.PublishEmailMessage()) for better performance.
The publisher logs important events:
"Publisher connected to RabbitMQ""Publisher connection error: %v""Publisher connection closed: %v""Published message on %s => %s""Publish error: %v"Email not sent:
Connection failures:
The email notification system is production-ready with: