Skip to content

Notifications Domain

The notifications domain provides multi-channel notification delivery (email, SMS, push) for TypeScript backends. It is structured in strict Clean Architecture layers with zero npm dependencies in the domain and application layers.

Terminal window
npx @backcap/cli add notifications

The Notification entity is the aggregate root. It tracks delivery status through immutable state transitions.

import { Notification } from "./domains/notifications/domain/entities/notification.entity";
const result = Notification.create({
id: crypto.randomUUID(),
channel: "email",
recipient: "user@example.com",
subject: "Welcome!",
body: "Thanks for signing up.",
});
if (result.isOk()) {
const notification = result.unwrap();
console.log(notification.status); // "pending"
const sent = notification.markSent();
console.log(sent.status); // "sent"
console.log(sent.sentAt); // Date
}
FieldTypeDescription
idstringUnique identifier (UUID)
channelNotificationChannelDelivery channel value object
recipientstringRecipient address (email, phone, device ID)
subjectstringNotification subject
bodystringNotification body content
status"pending" | "sent" | "failed"Current delivery status
sentAtDate | nullTimestamp when sent (null if pending/failed)

Notification.create() returns Result<Notification, InvalidChannel>.

State transitions:

  • markSent() — returns a new Notification with status "sent" and sentAt set
  • markFailed() — returns a new Notification with status "failed"
import { NotificationChannel } from "./domains/notifications/domain/value-objects/notification-channel.vo";
const result = NotificationChannel.create("email");
// Result<NotificationChannel, InvalidChannel>
if (result.isOk()) {
const channel = result.unwrap();
console.log(channel.value); // "email"
console.log(channel.isEmail()); // true
}

Supports: "email", "sms", "push". Returns InvalidChannel for anything else.

Error ClassConditionMessage
NotificationNotFoundNo notification for the given IDNotification not found with id: "<id>"
InvalidChannelChannel is not email/sms/pushInvalid notification channel: "<value>"
NotificationDeliveryFailedSend operation failedNotification delivery failed: "<reason>"
EventEmitted ByPayload
NotificationSentSendNotification use casenotificationId, channel, recipient, sentAt, occurredAt

Creates a notification, sends it via INotificationSender, marks it as sent or failed, and persists via INotificationRepository.

import { SendNotification } from "./domains/notifications/application/use-cases/send-notification.use-case";
const sendNotification = new SendNotification(notificationSender, notificationRepository);
const result = await sendNotification.execute({
channel: "email",
recipient: "user@example.com",
subject: "Welcome!",
body: "Thanks for signing up.",
});
// Result<{ output: { notificationId: string }; event: NotificationSent }, Error>

Possible failures: InvalidChannel, NotificationDeliveryFailed

Retrieves notifications for a given recipient.

import { GetNotifications } from "./domains/notifications/application/use-cases/get-notifications.use-case";
const getNotifications = new GetNotifications(notificationRepository);
const result = await getNotifications.execute({ recipient: "user@example.com" });
// Result<{ notifications: Notification[] }, Error>

Marks a notification as sent by transitioning its status to "sent" via markSent(). Despite its name, it sets the status to "sent" (not "read") — there is no separate read state in the domain model.

import { MarkAsRead } from "./domains/notifications/application/use-cases/mark-as-read.use-case";
const markAsRead = new MarkAsRead(notificationRepository);
const result = await markAsRead.execute({ notificationId: "abc-123" });
// Result<void, NotificationNotFound>
export interface INotificationSender {
send(notification: Notification): Promise<void>;
}

The sender is a thin delivery port — it dispatches via your email/SMS/push provider. It does not persist.

export interface INotificationRepository {
save(notification: Notification): Promise<void>;
findById(notificationId: string): Promise<Notification | null>;
findByRecipient(recipient: string): Promise<Notification[]>;
}
import {
createNotificationsService,
INotificationsService,
} from "./domains/notifications/contracts";
const notificationsService: INotificationsService = createNotificationsService({
notificationSender,
notificationRepository,
});
// INotificationsService interface:
// send(input): Promise<Result<SendNotificationOutput, Error>>
// getByRecipient(input): Promise<Result<GetNotificationsOutput, Error>>
// markAsRead(input): Promise<Result<void, Error>>

This is the only import consumers need. The internal use case classes are implementation details.

domains/notifications/
domain/
entities/notification.entity.ts
value-objects/notification-channel.vo.ts
errors/notification-not-found.error.ts
errors/invalid-channel.error.ts
errors/notification-delivery-failed.error.ts
events/notification-sent.event.ts
application/
use-cases/send-notification.use-case.ts
use-cases/get-notifications.use-case.ts
use-cases/mark-as-read.use-case.ts
ports/notification-sender.port.ts
ports/notification-repository.port.ts
dto/send-notification.dto.ts
dto/get-notifications.dto.ts
dto/mark-as-read.dto.ts
contracts/
notifications.contract.ts
notifications.factory.ts
index.ts
shared/
result.ts