diff --git a/src/ScadaLink.NotificationOutbox/Delivery/DeliveryOutcome.cs b/src/ScadaLink.NotificationOutbox/Delivery/DeliveryOutcome.cs
new file mode 100644
index 0000000..772e3b3
--- /dev/null
+++ b/src/ScadaLink.NotificationOutbox/Delivery/DeliveryOutcome.cs
@@ -0,0 +1,40 @@
+namespace ScadaLink.NotificationOutbox.Delivery;
+
+///
+/// Classification of a single delivery attempt. Transient failures are eligible for
+/// retry; permanent failures are terminal and not retried.
+///
+public enum DeliveryResult
+{
+ /// The notification was delivered successfully.
+ Success,
+
+ /// Delivery failed for a transient reason and may succeed on retry.
+ TransientFailure,
+
+ /// Delivery failed for a permanent reason and must not be retried.
+ PermanentFailure
+}
+
+///
+/// Result of a delivery attempt produced by an .
+///
+/// The classification of the attempt.
+///
+/// The concrete delivery targets used, snapshotted for audit. Set only on success.
+///
+/// A human-readable failure description. Set only on failure.
+public record DeliveryOutcome(DeliveryResult Result, string? ResolvedTargets, string? Error)
+{
+ /// Creates a successful outcome carrying the resolved delivery targets.
+ public static DeliveryOutcome Success(string resolvedTargets) =>
+ new(DeliveryResult.Success, resolvedTargets, null);
+
+ /// Creates a transient-failure outcome carrying an error description.
+ public static DeliveryOutcome Transient(string error) =>
+ new(DeliveryResult.TransientFailure, null, error);
+
+ /// Creates a permanent-failure outcome carrying an error description.
+ public static DeliveryOutcome Permanent(string error) =>
+ new(DeliveryResult.PermanentFailure, null, error);
+}
diff --git a/src/ScadaLink.NotificationOutbox/Delivery/INotificationDeliveryAdapter.cs b/src/ScadaLink.NotificationOutbox/Delivery/INotificationDeliveryAdapter.cs
new file mode 100644
index 0000000..54ced60
--- /dev/null
+++ b/src/ScadaLink.NotificationOutbox/Delivery/INotificationDeliveryAdapter.cs
@@ -0,0 +1,23 @@
+using ScadaLink.Commons.Entities.Notifications;
+using ScadaLink.Commons.Types.Enums;
+
+namespace ScadaLink.NotificationOutbox.Delivery;
+
+///
+/// Channel-specific delivery strategy for outbox notifications. Each adapter handles
+/// a single ; the outbox dispatcher selects the adapter
+/// matching a notification's type.
+///
+public interface INotificationDeliveryAdapter
+{
+ /// The notification channel this adapter delivers.
+ NotificationType Type { get; }
+
+ ///
+ /// Attempts delivery of the given notification and reports the classified outcome.
+ ///
+ /// The notification to deliver.
+ /// Token used to cancel the delivery attempt.
+ /// The outcome of the delivery attempt.
+ Task DeliverAsync(Notification notification, CancellationToken cancellationToken = default);
+}
diff --git a/src/ScadaLink.NotificationOutbox/NotificationOutboxOptions.cs b/src/ScadaLink.NotificationOutbox/NotificationOutboxOptions.cs
new file mode 100644
index 0000000..2e69d04
--- /dev/null
+++ b/src/ScadaLink.NotificationOutbox/NotificationOutboxOptions.cs
@@ -0,0 +1,26 @@
+namespace ScadaLink.NotificationOutbox;
+
+///
+/// Configuration options for the Notification Outbox component: dispatch cadence,
+/// batch sizing, stuck-message detection, terminal retention, and KPI windowing.
+///
+public class NotificationOutboxOptions
+{
+ /// Interval between dispatch sweeps that pick up pending notifications for delivery.
+ public TimeSpan DispatchInterval { get; set; } = TimeSpan.FromSeconds(10);
+
+ /// Maximum number of notifications claimed for delivery in a single dispatch sweep.
+ public int DispatchBatchSize { get; set; } = 100;
+
+ /// Age past which an in-progress notification is considered stuck and re-claimed.
+ public TimeSpan StuckAgeThreshold { get; set; } = TimeSpan.FromMinutes(10);
+
+ /// Retention period for notifications in a terminal state before they are purged.
+ public TimeSpan TerminalRetention { get; set; } = TimeSpan.FromDays(365);
+
+ /// Interval between background purge sweeps of terminal notifications.
+ public TimeSpan PurgeInterval { get; set; } = TimeSpan.FromDays(1);
+
+ /// Trailing window used to compute the delivered-notifications throughput KPI.
+ public TimeSpan DeliveredKpiWindow { get; set; } = TimeSpan.FromMinutes(1);
+}
diff --git a/tests/ScadaLink.NotificationOutbox.Tests/Delivery/DeliveryOutcomeTests.cs b/tests/ScadaLink.NotificationOutbox.Tests/Delivery/DeliveryOutcomeTests.cs
new file mode 100644
index 0000000..3a9d826
--- /dev/null
+++ b/tests/ScadaLink.NotificationOutbox.Tests/Delivery/DeliveryOutcomeTests.cs
@@ -0,0 +1,36 @@
+using ScadaLink.NotificationOutbox.Delivery;
+
+namespace ScadaLink.NotificationOutbox.Tests.Delivery;
+
+public class DeliveryOutcomeTests
+{
+ [Fact]
+ public void Success_SetsResolvedTargets_AndNoError()
+ {
+ var outcome = DeliveryOutcome.Success("targets");
+
+ Assert.Equal(DeliveryResult.Success, outcome.Result);
+ Assert.Equal("targets", outcome.ResolvedTargets);
+ Assert.Null(outcome.Error);
+ }
+
+ [Fact]
+ public void Transient_SetsError_AndNoResolvedTargets()
+ {
+ var outcome = DeliveryOutcome.Transient("e");
+
+ Assert.Equal(DeliveryResult.TransientFailure, outcome.Result);
+ Assert.Equal("e", outcome.Error);
+ Assert.Null(outcome.ResolvedTargets);
+ }
+
+ [Fact]
+ public void Permanent_SetsError_AndNoResolvedTargets()
+ {
+ var outcome = DeliveryOutcome.Permanent("e");
+
+ Assert.Equal(DeliveryResult.PermanentFailure, outcome.Result);
+ Assert.Equal("e", outcome.Error);
+ Assert.Null(outcome.ResolvedTargets);
+ }
+}
diff --git a/tests/ScadaLink.NotificationOutbox.Tests/NotificationOutboxOptionsTests.cs b/tests/ScadaLink.NotificationOutbox.Tests/NotificationOutboxOptionsTests.cs
new file mode 100644
index 0000000..c9d0cf6
--- /dev/null
+++ b/tests/ScadaLink.NotificationOutbox.Tests/NotificationOutboxOptionsTests.cs
@@ -0,0 +1,17 @@
+namespace ScadaLink.NotificationOutbox.Tests;
+
+public class NotificationOutboxOptionsTests
+{
+ [Fact]
+ public void Defaults_AreExpectedValues()
+ {
+ var options = new NotificationOutboxOptions();
+
+ Assert.Equal(TimeSpan.FromSeconds(10), options.DispatchInterval);
+ Assert.Equal(100, options.DispatchBatchSize);
+ Assert.Equal(TimeSpan.FromMinutes(10), options.StuckAgeThreshold);
+ Assert.Equal(TimeSpan.FromDays(365), options.TerminalRetention);
+ Assert.Equal(TimeSpan.FromDays(1), options.PurgeInterval);
+ Assert.Equal(TimeSpan.FromMinutes(1), options.DeliveredKpiWindow);
+ }
+}