sendTemplateEmail(EmailTemplateRequestDTO request) {
+ if (templateEngine == null) {
+ return Mono.error(new UnsupportedOperationException(
+ "Template email not supported. Configure a NotificationTemplateEngine bean."));
+ }
+
+ return templateEngine.render(request.getTemplateId(), request.getTemplateVariables())
+ .flatMap(renderedHtml -> {
+ EmailRequestDTO emailRequest = EmailRequestDTO.builder()
+ .from(request.getFrom())
+ .to(request.getTo())
+ .cc(request.getCc())
+ .bcc(request.getBcc())
+ .subject(request.getSubject())
+ .html(renderedHtml)
+ .build();
+ return emailProvider.sendEmail(emailRequest);
+ })
+ .doOnError(e -> log.error("Failed to send template email '{}': {}",
+ request.getTemplateId(), e.getMessage()));
+ }
}
\ No newline at end of file
diff --git a/fireflyframework-notifications-core/src/main/java/org/fireflyframework/notifications/core/services/preferences/InMemoryNotificationPreferenceService.java b/fireflyframework-notifications-core/src/main/java/org/fireflyframework/notifications/core/services/preferences/InMemoryNotificationPreferenceService.java
new file mode 100644
index 0000000..11f9296
--- /dev/null
+++ b/fireflyframework-notifications-core/src/main/java/org/fireflyframework/notifications/core/services/preferences/InMemoryNotificationPreferenceService.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2024-2026 Firefly Software Solutions Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.fireflyframework.notifications.core.services.preferences;
+
+import lombok.extern.slf4j.Slf4j;
+import org.fireflyframework.notifications.interfaces.dtos.preferences.NotificationPreferenceDTO;
+import reactor.core.publisher.Mono;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * In-memory implementation of {@link NotificationPreferenceService}.
+ *
+ * Suitable for development, testing, and single-instance deployments.
+ * For production multi-instance deployments, use a persistent-backed implementation.
+ */
+@Slf4j
+public class InMemoryNotificationPreferenceService implements NotificationPreferenceService {
+
+ private final ConcurrentHashMap store = new ConcurrentHashMap<>();
+
+ @Override
+ public Mono getPreferences(String userId) {
+ return Mono.justOrEmpty(store.get(userId))
+ .switchIfEmpty(Mono.just(NotificationPreferenceDTO.builder()
+ .userId(userId)
+ .emailEnabled(true)
+ .smsEnabled(true)
+ .pushEnabled(true)
+ .build()));
+ }
+
+ @Override
+ public Mono updatePreferences(String userId, NotificationPreferenceDTO preferences) {
+ preferences.setUserId(userId);
+ store.put(userId, preferences);
+ log.debug("Updated notification preferences for user: {}", userId);
+ return Mono.just(preferences);
+ }
+
+ @Override
+ public Mono isChannelEnabled(String userId, String channel) {
+ return getPreferences(userId)
+ .map(prefs -> prefs.isChannelEnabled(channel));
+ }
+}
diff --git a/fireflyframework-notifications-core/src/main/java/org/fireflyframework/notifications/core/services/preferences/NotificationPreferenceService.java b/fireflyframework-notifications-core/src/main/java/org/fireflyframework/notifications/core/services/preferences/NotificationPreferenceService.java
new file mode 100644
index 0000000..e94ef66
--- /dev/null
+++ b/fireflyframework-notifications-core/src/main/java/org/fireflyframework/notifications/core/services/preferences/NotificationPreferenceService.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024-2026 Firefly Software Solutions Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.fireflyframework.notifications.core.services.preferences;
+
+import org.fireflyframework.notifications.interfaces.dtos.preferences.NotificationPreferenceDTO;
+import reactor.core.publisher.Mono;
+
+/**
+ * Service for managing user notification preferences.
+ *
+ * Implementations may store preferences in memory, R2DBC, or a cache backend.
+ * The default in-memory implementation is provided for development and testing.
+ */
+public interface NotificationPreferenceService {
+
+ /**
+ * Get notification preferences for a user.
+ * Returns default (all channels enabled) if no preferences are stored.
+ */
+ Mono getPreferences(String userId);
+
+ /**
+ * Update notification preferences for a user.
+ */
+ Mono updatePreferences(String userId, NotificationPreferenceDTO preferences);
+
+ /**
+ * Check if a specific channel is enabled for a user.
+ */
+ Mono isChannelEnabled(String userId, String channel);
+}
diff --git a/fireflyframework-notifications-core/src/main/java/org/fireflyframework/notifications/core/services/template/FreemarkerNotificationTemplateEngine.java b/fireflyframework-notifications-core/src/main/java/org/fireflyframework/notifications/core/services/template/FreemarkerNotificationTemplateEngine.java
new file mode 100644
index 0000000..ea409c6
--- /dev/null
+++ b/fireflyframework-notifications-core/src/main/java/org/fireflyframework/notifications/core/services/template/FreemarkerNotificationTemplateEngine.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2024-2026 Firefly Software Solutions Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.fireflyframework.notifications.core.services.template;
+
+import freemarker.cache.ClassTemplateLoader;
+import freemarker.cache.FileTemplateLoader;
+import freemarker.cache.MultiTemplateLoader;
+import freemarker.cache.TemplateLoader;
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.TemplateExceptionHandler;
+import lombok.extern.slf4j.Slf4j;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * FreeMarker-based implementation of {@link NotificationTemplateEngine}.
+ *
+ * Loads templates from a configurable classpath prefix (default: {@code /notification-templates})
+ * and an optional filesystem directory. Template files use the {@code .ftl} extension by convention.
+ */
+@Slf4j
+public class FreemarkerNotificationTemplateEngine implements NotificationTemplateEngine {
+
+ private final Configuration configuration;
+
+ public FreemarkerNotificationTemplateEngine(String classpathPrefix, String filesystemDir) {
+ this.configuration = buildConfiguration(classpathPrefix, filesystemDir);
+ log.info("FreemarkerNotificationTemplateEngine initialized (classpath: {}, filesystem: {})",
+ classpathPrefix, filesystemDir);
+ }
+
+ public FreemarkerNotificationTemplateEngine() {
+ this("/notification-templates", null);
+ }
+
+ @Override
+ public Mono render(String templateId, Map variables) {
+ return Mono.fromCallable(() -> {
+ String templateName = templateId.endsWith(".ftl") ? templateId : templateId + ".ftl";
+ Template template = configuration.getTemplate(templateName);
+ StringWriter writer = new StringWriter();
+ template.process(variables != null ? variables : Map.of(), writer);
+ return writer.toString();
+ })
+ .subscribeOn(Schedulers.boundedElastic())
+ .doOnError(e -> log.error("Failed to render template '{}': {}", templateId, e.getMessage()));
+ }
+
+ private Configuration buildConfiguration(String classpathPrefix, String filesystemDir) {
+ Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
+ cfg.setDefaultEncoding(StandardCharsets.UTF_8.name());
+ cfg.setLocale(Locale.US);
+ cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
+
+ List loaders = new ArrayList<>();
+ loaders.add(new ClassTemplateLoader(getClass().getClassLoader(),
+ classpathPrefix != null ? classpathPrefix : "/notification-templates"));
+
+ if (filesystemDir != null) {
+ try {
+ File dir = new File(filesystemDir);
+ if (dir.isDirectory()) {
+ loaders.add(new FileTemplateLoader(dir));
+ }
+ } catch (IOException e) {
+ log.warn("Could not configure filesystem template loader for '{}': {}", filesystemDir, e.getMessage());
+ }
+ }
+
+ if (loaders.size() == 1) {
+ cfg.setTemplateLoader(loaders.get(0));
+ } else {
+ cfg.setTemplateLoader(new MultiTemplateLoader(loaders.toArray(new TemplateLoader[0])));
+ }
+
+ return cfg;
+ }
+}
diff --git a/fireflyframework-notifications-core/src/main/java/org/fireflyframework/notifications/core/services/template/NotificationTemplateEngine.java b/fireflyframework-notifications-core/src/main/java/org/fireflyframework/notifications/core/services/template/NotificationTemplateEngine.java
new file mode 100644
index 0000000..7e6c142
--- /dev/null
+++ b/fireflyframework-notifications-core/src/main/java/org/fireflyframework/notifications/core/services/template/NotificationTemplateEngine.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024-2026 Firefly Software Solutions Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.fireflyframework.notifications.core.services.template;
+
+import reactor.core.publisher.Mono;
+
+import java.util.Map;
+
+/**
+ * Reactive template engine for rendering notification templates.
+ *
+ * Templates are resolved by ID and rendered with variable substitution.
+ * Implementations may use FreeMarker, Mustache, Thymeleaf, or any other engine.
+ */
+public interface NotificationTemplateEngine {
+
+ /**
+ * Render a template by its ID with the given variables.
+ *
+ * @param templateId the template identifier (e.g., "welcome-email", "password-reset")
+ * @param variables the variable map for template substitution
+ * @return a Mono emitting the rendered content (typically HTML)
+ */
+ Mono render(String templateId, Map variables);
+}
diff --git a/fireflyframework-notifications-core/src/main/java/org/fireflyframework/notifications/interfaces/dtos/email/v1/EmailTemplateRequestDTO.java b/fireflyframework-notifications-core/src/main/java/org/fireflyframework/notifications/interfaces/dtos/email/v1/EmailTemplateRequestDTO.java
new file mode 100644
index 0000000..609964b
--- /dev/null
+++ b/fireflyframework-notifications-core/src/main/java/org/fireflyframework/notifications/interfaces/dtos/email/v1/EmailTemplateRequestDTO.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2024-2026 Firefly Software Solutions Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.fireflyframework.notifications.interfaces.dtos.email.v1;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Request DTO for sending templated emails.
+ *
+ * The {@code templateId} identifies a template (e.g., "welcome-email"),
+ * and {@code templateVariables} are substituted into the template before sending.
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class EmailTemplateRequestDTO {
+
+ private String templateId;
+
+ @Builder.Default
+ private Map templateVariables = new HashMap<>();
+
+ private String from;
+ private String to;
+
+ @Builder.Default
+ private List cc = new ArrayList<>();
+
+ @Builder.Default
+ private List bcc = new ArrayList<>();
+
+ private String subject;
+}
diff --git a/fireflyframework-notifications-core/src/main/java/org/fireflyframework/notifications/interfaces/dtos/preferences/NotificationPreferenceDTO.java b/fireflyframework-notifications-core/src/main/java/org/fireflyframework/notifications/interfaces/dtos/preferences/NotificationPreferenceDTO.java
new file mode 100644
index 0000000..d32210d
--- /dev/null
+++ b/fireflyframework-notifications-core/src/main/java/org/fireflyframework/notifications/interfaces/dtos/preferences/NotificationPreferenceDTO.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2024-2026 Firefly Software Solutions Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.fireflyframework.notifications.interfaces.dtos.preferences;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * DTO representing a user's notification channel preferences.
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class NotificationPreferenceDTO {
+
+ private String userId;
+
+ @Builder.Default
+ private boolean emailEnabled = true;
+
+ @Builder.Default
+ private boolean smsEnabled = true;
+
+ @Builder.Default
+ private boolean pushEnabled = true;
+
+ @Builder.Default
+ private Map channels = new HashMap<>();
+
+ /**
+ * Check if a specific channel is enabled.
+ * Falls back to the top-level channel toggle if no specific override exists.
+ */
+ public boolean isChannelEnabled(String channel) {
+ if (channels.containsKey(channel)) {
+ return channels.get(channel);
+ }
+ return switch (channel.toLowerCase()) {
+ case "email" -> emailEnabled;
+ case "sms" -> smsEnabled;
+ case "push" -> pushEnabled;
+ default -> true;
+ };
+ }
+}