How Dokaai Uses Domain-Driven Design to Build Nudge for Scale
Building reliable notification systems takes more than sending messages. Discover how DokaAI uses Domain-Driven Design, clean architecture, and event-driven systems to build scalable, reliable Nudge.

Atul Joshi
16 min read
Sending a notification looks simple from the outside.
An event happens, a message is created, and a provider delivers it.
But a production notification platform must answer far more difficult questions:
- Which customer should receive the message?
- Which channel should be used?
- Has the customer opted out?
- Is the message allowed during the current time window?
- Which template, language, and provider should be selected?
- What should happen when a provider fails?
- Can the message be retried without creating a duplicate?
- How should delivery, usage, and cost be measured?
- Can product, engineering, and growth teams change the workflow safely?
These are not merely technical questions. They are business rules.
As a notification product grows, those rules multiply. If they are scattered across controllers, database queries, provider integrations, background jobs, and user-interface conditions, even a small product change can become risky.
Dokaai uses Domain-Driven Design, or DDD, to keep that complexity understandable.
DDD gives Nudge a shared business language, clear system boundaries, and a structure in which important rules have an explicit home. Combined with clean architecture and event-driven processing, it allows Nudge to grow without turning every new capability into a cross-system rewrite.
It also creates something increasingly valuable in modern engineering: a codebase that humans and AI agents can understand more accurately.
Why notification infrastructure becomes complex so quickly
A notification platform sits at the intersection of multiple business capabilities.
It manages customers and audiences. It connects to external providers. It evaluates preferences and policies. It renders personalized content. It schedules work, retries failed deliveries, records usage, and reports outcomes.
The first version of such a system can often be built with a few endpoints and provider-specific functions. The real challenge appears later.
A new channel introduces different validation rules. A new provider has different delivery states. Marketing needs scheduled campaigns, while engineering needs low-latency transactional alerts. Customers need preferences, quiet hours, localization, and compliance controls. Finance needs accurate usage records.
Without strong boundaries, these concerns begin leaking into one another.
A provider change unexpectedly affects templates. Billing logic becomes coupled to delivery logic. The meaning of “customer” changes between modules. Retry behavior becomes different for every channel. Teams become afraid to change the system because no one can predict the full impact.
DDD helps prevent that architectural drift.
What Domain-Driven Design means
Domain-Driven Design is an approach to software development that organizes a system around the business it serves.
The central idea is simple: important software should model real business concepts instead of forcing the business into a collection of technical tables, endpoints, and utility functions.
Microsoft’s architecture guidance describes DDD as a framework for designing cohesive, loosely coupled services around business capabilities. It emphasizes that system boundaries should come from domain analysis rather than arbitrary technical layers. Microsoft: Domain analysis for microservices
DDD includes several strategic and tactical concepts. Dokaai applies them pragmatically rather than treating them as rigid ceremony.
Ubiquitous language
A ubiquitous language is a shared vocabulary used consistently by product teams, domain experts, engineers, documentation, and software.
In Nudge, concepts such as notification handler, channel, provider integration, customer preference, target audience, billing period, and usage pool have specific meanings.
That consistency matters.
If one team uses “campaign” to mean a reusable configuration while another uses it to mean a single execution, requirements become ambiguous. The ambiguity eventually becomes inconsistent behavior.
A shared language reduces the distance between a product decision and its implementation.
Bounded contexts
A bounded context creates a clear boundary around one business model.
Dokaai separates major business capabilities into contexts such as:
- Organization and project management
- Customer and audience management
- Notification configuration and delivery
- Subscription and usage management
A “user” in organization management is a team member who can access Dokaai. A “customer” in customer management is an end recipient who may receive a notification.
Both can represent people, but they are not interchangeable business concepts.
Keeping them separate prevents one oversized model from becoming responsible for every possible interpretation of a person, account, or identity.
Entities
An entity represents a business concept with an identity and a lifecycle.
A notification handler, customer, provider integration, project, or subscription can continue to represent the same business concept even when its properties change.
Thinking in entities encourages teams to ask what the object is allowed to do, how it changes, and which rules must remain true throughout its lifecycle.
Value objects
A value object represents a meaningful value without requiring its own independent identity.
Email addresses, phone numbers, time zones, recurrence rules, billing periods, monetary amounts, and delivery limits are good examples.
A raw string can contain almost anything. A domain value communicates meaning and provides one place for validation and normalization.
This prevents the same rule from being implemented differently across endpoints, jobs, imports, and provider adapters.
Aggregates and consistency boundaries
Some business changes involve several related pieces of information that must remain consistent.
DDD groups these related concepts behind a controlled boundary. Instead of allowing any part of the system to modify internal state directly, changes pass through domain behavior that protects the business rules.
Consider publishing a notification workflow. The system may need to verify that required templates exist, channel configuration is valid, provider connections are available, and required variables are defined.
Treating publication as a domain operation makes those rules part of the product model instead of leaving them as optional checks in a user interface.
Domain services
Some business operations do not belong naturally to a single entity.
Choosing a permitted channel, evaluating a delivery policy, calculating usage, or deciding whether a notification can be sent may depend on multiple pieces of domain information.
Domain services give these decisions an explicit and testable home.
Domain events
A business event records that something meaningful has happened.
Examples include a notification being requested, a delivery succeeding, a provider rejecting a message, a preference changing, or a usage record being created.
Events allow another part of the system to react without placing every responsibility inside the original operation.
This becomes especially valuable when scaling notification processing.
DDD at Dokaai
Dokaai’s product model reflects the way customers actually configure and operate communication workflows.
An organization owns projects. Projects contain customer data and notification configurations. Customers can belong to audiences and segments. Notification handlers define how communication should be produced. Provider integrations connect those handlers to external delivery services. Subscription rules measure the resulting usage.
Each area has its own vocabulary and responsibilities.
This gives the platform a map.
When a feature concerns customer segmentation, its behavior belongs with customer and audience concepts. When it concerns message eligibility, it belongs with notification preferences and send policies. When it concerns quota consumption, it belongs with subscription and usage.
The goal is not to create more folders or architectural terminology. The goal is to reduce the number of places an engineer must inspect before understanding a business change.
Clean architecture: keeping business rules independent
DDD defines the business model. Clean architecture protects it.
At a high level, Nudge separates responsibilities into distinct layers.
The domain layer
The domain layer contains the business language and rules.
It should understand concepts such as customers, notification handlers, preferences, policies, schedules, and billing periods. It should not need to understand HTTP controllers, database clients, queue libraries, or a particular notification vendor.
The application layer
The application layer coordinates use cases.
It answers questions such as:
- What steps are required to create a notification handler?
- What must happen when a notification event is received?
- Which domain rules must be evaluated before delivery?
- Which external capabilities must be called?
- What outcome should be returned?
This layer orchestrates work without redefining the underlying business rules.
The infrastructure layer
The infrastructure layer connects the product model to external systems.
It contains implementations for databases, queues, caches, providers, APIs, event streams, and real-time connections.
The domain can request that a notification be delivered without being designed around one email or SMS vendor. Provider-specific behavior remains at the edge of the system.
The interface layer
HTTP APIs, background workers, event consumers, and real-time gateways are different ways to enter the same product capabilities.
They translate external input into application actions and translate results back into the appropriate response.
This separation helps Dokaai evolve technologies without rewriting the business model.
A practical example: sending a notification
Imagine that an online marketplace needs to send an order update.
Without a domain-oriented structure, the request might immediately select a provider, query multiple tables, render a template, check an opt-out field, send the message, and update billing in one large operation.
That approach may work initially, but each new rule makes the operation harder to understand.
In a domain-oriented flow, the responsibilities are clearer:
- The incoming event identifies the project and notification handler.
- The customer and audience context resolves the intended recipient.
- The notification context evaluates preferences, DND rules, channel configuration, and template requirements.
- The application coordinates the approved delivery plan.
- Infrastructure selects and calls the appropriate provider.
- Delivery state is recorded and observable.
- Subscription and usage logic accounts for the relevant activity.
The important difference is not the number of steps. It is that each decision has an owner.
When a preference rule changes, the team knows where it belongs. When a new provider is introduced, the provider integration can change without redefining the meaning of a notification handler. When billing evolves, delivery behavior does not need to absorb financial rules.
The technology behind the architecture
DDD is not tied to one programming language or framework. It is a way of structuring knowledge and responsibilities.
Dokaai uses a modern TypeScript stack to implement these principles:
- Node.js and TypeScript provide a strongly typed foundation for APIs, services, and workers.
- Nx and pnpm support a modular monorepo with explicit project boundaries and targeted builds.
- Express-based services and dependency injection connect application use cases to APIs and infrastructure.
- Prisma and PostgreSQL support persistent business and delivery state.
- Redis and BullMQ support caching, asynchronous jobs, retries, delayed work, and scheduled execution.
- Kafka supports event-driven communication between services and business workflows.
- Socket.IO with Redis-backed coordination supports real-time product experiences and horizontally scalable connections.
- OpenAPI creates explicit, language-independent API contracts.
- Jest and behavior-oriented tests help verify both technical behavior and business rules.
- Containerized services allow API, authentication, worker, and real-time workloads to scale according to their different runtime needs.
The technology is important, but it remains secondary to the boundaries.
A queue cannot fix an unclear business model. A database cannot decide which context owns a rule. A microservice does not become independent merely because it is deployed separately.
DDD gives those technologies a coherent product structure.
How Dokaai made Nudge scalable
Nudge is Dokaai’s notification infrastructure for email, SMS, WhatsApp, push, and in-app communication. It brings templates, audiences, preferences, routing, retries, providers, and delivery visibility into one platform. Explore Nudge
Its scalability comes from several complementary design decisions.
Separate synchronous requests from asynchronous work
API requests should not remain open while every downstream delivery step is completed.
Nudge separates request handling from background execution. Work can be accepted, validated, and scheduled before specialized workers process delivery tasks.
This protects the user-facing API from slow provider responses and allows worker capacity to scale independently.
Use explicit queues for distinct workloads
Notification execution, template preparation, scheduled activity, uploads, audience processing, and real-time delivery do not all have the same performance profile.
Separating workloads makes it possible to tune concurrency, retry behavior, prioritization, and resource allocation for each kind of work.
A large audience import should not prevent a critical transactional notification from being processed.
Treat retries as part of delivery design
External providers fail. Connections time out. Rate limits change. Temporary errors are inevitable.
A scalable notification system must define what can be retried, how attempts are tracked, when work should be delayed, and how duplicate delivery is avoided.
Stable job identities, explicit execution states, and controlled retry paths make failure recoverable instead of mysterious.
Keep providers behind adapters
Nudge supports multiple delivery channels and provider integrations.
Provider-specific request formats, credentials, responses, and failure states are translated at the infrastructure boundary. The core notification model does not need to be rewritten whenever a provider is added or replaced.
This is essential for provider choice, fallback strategies, and avoiding vendor lock-in.
Scale real-time connections independently
In-app and real-time notifications have different runtime needs from standard APIs and background workers.
By separating real-time connection handling and using shared coordination infrastructure, Nudge can scale connected clients without forcing every other service to scale in the same way.
Make policies first-class concepts
Preferences, DND windows, limits, schedules, and delivery rules are not scattered conditions. They are product concepts.
Making them explicit improves consistency. The same rule can be applied whether a notification originated from an API request, a scheduled workflow, or an event.
Build observability around business outcomes
Infrastructure metrics are useful, but customers ultimately care about business outcomes:
- Was the notification accepted?
- Was it allowed?
- Which channel and provider were selected?
- Was delivery attempted?
- Did it succeed, fail, or require a retry?
- How long did it take?
- How was usage recorded?
A domain-oriented system makes those states easier to name, track, and explain.
Why DDD helps teams ship features faster
Architecture should make everyday development easier.
With clear bounded contexts, a feature request has a more predictable scope. Engineers can find the responsible module, understand its vocabulary, change the relevant rules, and run focused tests.
This reduces several common sources of engineering waste:
- Reimplementing the same validation in multiple places
- Changing unrelated modules for a local feature
- Creating inconsistent meanings for the same term
- Allowing provider details to leak into product logic
- Debugging workflows with no explicit state model
- Repeating business decisions across APIs and workers
- Requiring full-system knowledge for every small change
DDD does not eliminate complexity. It makes complexity visible and gives it a deliberate structure.
That is what improves delivery speed over time.
The economic cost of unclear software
Poor software structure is not merely an engineering inconvenience.
The Consortium for Information & Software Quality estimated that poor software quality cost the United States at least $2.41 trillion in 2022, with accumulated technical debt estimated at approximately $1.52 trillion. CISQ: Cost of Poor Software Quality in the US
Stripe’s Developer Coefficient report estimated that developers spent about four hours per week dealing with “bad code,” representing nearly $85 billion in annual global opportunity cost. Stripe: The Developer Coefficient
DDD is not a universal cure for software defects, and those industry-wide figures should not be attributed to architecture alone. However, DDD directly addresses one of the persistent causes of expensive software: a mismatch between business intent and system behavior.
When business rules have no clear owner, bugs emerge at the boundaries.
A preference is respected in one delivery path but ignored in another. A customer is interpreted differently by two services. A billing operation counts an attempt while another part of the system counts only successful delivery. A provider-specific status leaks into product logic and is treated as a universal state.
These problems consume engineering time because the failure is rarely isolated to one line of code. The team must first reconstruct what the system was supposed to mean.
A clear domain model reduces that reconstruction cost.
It helps teams prevent defects, diagnose failures more quickly, and add new capabilities without repeatedly rediscovering old decisions.
DDD as a catalyst for AI agents and engineering skills
AI-assisted engineering changes how teams interact with a codebase, but it does not remove the need for architecture.
In fact, architecture becomes more important.
An AI agent can generate code quickly. The harder problem is determining where a change belongs, which rules must remain true, what language should be used, and which parts of the system must not be affected.
DDD supplies this missing context.
Smaller and more relevant context
An agent working on notification preferences should not need the complete billing, organization, authentication, and provider implementation in its context.
Bounded contexts create smaller, higher-signal working areas. This helps an agent focus on the relevant domain instead of drawing conclusions from unrelated code.
More precise task instructions
“Update the notification preference policy” is more precise than “add another condition before sending.”
When product language is reflected in the architecture, prompts can describe business intent directly. The agent has a better chance of locating the correct responsibility and producing a change that fits the existing model.
Better skill routing
Engineering skills and agent playbooks can be mapped to stable domains.
A notification-delivery skill can understand delivery states, policies, retries, and provider boundaries. A subscription skill can focus on billing periods, usage pools, and plan rules.
The skill does not need to relearn the entire platform for every task.
Safer generated changes
Entities, value objects, application use cases, interfaces, and focused tests give AI agents visible constraints.
Instead of inventing a new validation path, an agent can reuse an existing domain concept. Instead of directly coupling to a provider, it can work through an established port. Instead of changing a shared database model casually, it can identify the context that owns the data.
Faster review
Consistent architecture also improves human review of AI-generated work.
Reviewers can ask:
- Is this change in the correct bounded context?
- Does it use the established domain language?
- Does it protect existing invariants?
- Is provider-specific behavior contained at the infrastructure edge?
- Are application orchestration and domain decisions still separated?
- Do the tests describe the intended business outcome?
These questions are more reliable than reviewing generated code only for syntax and style.
Parallel work with lower collision risk
Clear context and package boundaries make it easier for multiple engineers or agents to work in parallel.
One task can evolve customer segmentation while another improves provider delivery behavior, provided their contracts remain stable.
The architecture acts as a coordination mechanism.
DDD therefore becomes a catalyst for AI agents and reusable engineering skills. It does not make an agent inherently correct. It gives the agent a better map, a smaller scope, and clearer rules.
What DDD does not mean
DDD should not become unnecessary ceremony.
Not every database record needs a complex entity. Not every small operation needs a domain service. Not every context must become a separately deployed microservice.
Simple CRUD behavior can remain simple.
The most valuable DDD patterns belong where business rules are complex, important, or frequently changing. Notification delivery, policies, customer preferences, scheduling, provider orchestration, and usage accounting are strong candidates because mistakes in these areas directly affect reliability, customer trust, and cost.
The objective is not to maximize the number of patterns. It is to make the product easier to reason about.
The impact for Dokaai and its customers
For Dokaai, a domain-oriented architecture creates a foundation for long-term product development.
It helps the engineering team:
- Add channels and providers without rewriting core notification behavior
- Introduce new policies without duplicating rules across delivery paths
- Scale APIs, workers, and real-time services independently
- Diagnose failures using meaningful business states
- Keep tests aligned with product behavior
- Give AI agents better context for implementation and review
- Reduce the blast radius of changes
- Turn product vocabulary into durable technical boundaries
For customers, those engineering qualities appear as product capabilities:
- More reliable notification delivery
- Faster introduction of new workflows
- Consistent preferences and governance
- Greater provider flexibility
- Better visibility into delivery outcomes
- Reduced integration and maintenance overhead
- A platform that can grow without requiring customers to rebuild their notification infrastructure
Summary
Domain-Driven Design helps Dokaai manage the real complexity behind customer communication.
The key principles are straightforward:
- Use one shared language across product, documentation, and engineering.
- Divide the system according to business capabilities.
- Keep important rules close to the domain concepts they protect.
- Separate business decisions from databases, queues, APIs, and providers.
- Use asynchronous and event-driven infrastructure where workloads need to scale independently.
- Make failures, retries, policies, and delivery states explicit.
- Give humans and AI agents clear boundaries for making safe changes.
The result is not simply cleaner code.
It is a platform that can evolve more quickly while remaining understandable.
Conclusion
Software becomes expensive when every change requires rediscovering how the system works.
DDD reduces that cost by turning business knowledge into architecture. It gives concepts clear names, rules clear owners, and product capabilities clear boundaries.
For Nudge, this foundation supports reliable multi-channel notifications, asynchronous delivery, customer preferences, provider flexibility, real-time experiences, usage tracking, and continued product expansion.
It also prepares Dokaai for the next generation of software development, in which engineers collaborate with AI agents and specialized skills. Those tools perform best when the system provides strong context, explicit contracts, and predictable boundaries.
That is how Dokaai approaches scale: not only by processing more messages, but by making the platform safer and faster to evolve.
Build your notification infrastructure with Nudge
Nudge gives product, engineering, and growth teams one platform for email, SMS, WhatsApp, push, and in-app notifications.
Connect your providers, define reusable notification handlers, manage audiences and preferences, and scale delivery without rebuilding the same infrastructure inside every product.
Explore Nudge or read the Dokaai documentation to get started.

