Your API Doesn't Need More Services, It Needs Events
If every new feature forces you to modify five different services, you probably have a coupling problem. Learn how Event-Driven Architecture helps decouple modules and scale NestJS applications.

There is a point in every growing application where a simple use case starts turning into a chain of responsibilities that becomes increasingly difficult to maintain.
Imagine a user registration flow.
At first, it looks simple:
await userRepository.create(user);
A few months later, the same flow looks more like this:
await userRepository.create(user);
await emailService.sendWelcomeEmail(user);
await auditService.registerUserCreation(user);
await analyticsService.trackNewUser(user);
await crmService.createContact(user);
await notificationService.notifyAdmins(user);
The problem is not the code itself.
The problem is that your user module now knows too much.
It knows how to send emails.
It knows how to register audits.
It knows how to update analytics.
It knows how to integrate with external platforms.
Every new integration increases coupling.
Every new requirement forces changes to code that was already working.
Eventually, something as simple as creating a user becomes a high-risk operation.
This is where Event-Driven Architecture becomes valuable.
The Real Problem: Hidden Dependencies
When we analyze systems that become difficult to maintain, we often find the same structure:
UserService
├── EmailService
├── AuditService
├── AnalyticsService
├── CRMService
└── NotificationService
Every dependency increases:
- Complexity
- Coupling
- Testing effort
- Risk of regressions
The result is that a seemingly small change impacts multiple areas of the system.
The Core Idea Behind Event-Driven Architecture
Instead of executing every action directly, the module publishes an event.
UserService
│
▼
UserCreatedEvent
│
┌────┼────┬─────┐
▼ ▼ ▼ ▼
Email Audit CRM Analytics
The service no longer needs to know who consumes the event.
It simply communicates something that happened.
this.eventBus.publish(
new UserCreatedEvent(user.id, user.email),
);
Nothing else.
Events Represent Facts
One of the most common mistakes is modeling events as commands.
Bad:
SendWelcomeEmailEvent
Good:
UserCreatedEvent
The distinction is important.
Events describe something that already happened.
They do not express intent.
Think of them as business facts.
UserCreatedEvent
OrderPaidEvent
InvoiceGeneratedEvent
SubscriptionCanceledEvent
Implementing It in NestJS
NestJS provides first-class support for events through CQRS.
Define the event:
export class UserCreatedEvent {
constructor(
public readonly userId: string,
public readonly email: string,
) {}
}
Publish the event:
this.eventBus.publish(
new UserCreatedEvent(
user.id,
user.email,
),
);
Create an event handler:
@EventsHandler(UserCreatedEvent)
export class SendWelcomeEmailHandler
implements IEventHandler<UserCreatedEvent>
{
async handle(event: UserCreatedEvent) {
await this.emailService.sendWelcomeEmail(
event.email,
);
}
}
Now the user module no longer knows anything about email delivery.
One Event, Multiple Consumers
This is one of the most powerful benefits of the pattern.
The same event can be consumed by multiple modules.
UserCreatedEvent
├── SendWelcomeEmailHandler
├── RegisterAuditHandler
├── SyncCRMHandler
└── GenerateMetricsHandler
And the producer never changes.
When Should You Use Events?
Not everything should become an event.
Events are especially useful when:
- Side effects exist.
- Multiple consumers are involved.
- Modules evolve independently.
- The process can be asynchronous.
Examples:
✅ User registration
✅ Order creation
✅ Invoice generation
✅ Inventory updates
✅ Notifications
When Should You Avoid Events?
If an operation is required to complete a transaction:
CreateOrder
└── ValidateStock
Do not convert it into an event.
It belongs to the primary business flow.
Events are excellent for decoupling.
They are not a way to hide critical business logic.
What You Gain in Practice
| Benefit | Impact |
|---|---|
| Lower Coupling | Fewer cross-module changes |
| Scalability | New consumers without modifying producers |
| Maintainability | Simpler codebases |
| Reusability | One event feeds multiple processes |
| Evolution | Independent integrations |
A Natural Fit for Hexagonal Architecture
If you already use Hexagonal Architecture, events fit naturally.
The domain raises events.
The application publishes them.
Adapters react to them.
This keeps the business core clean and free from infrastructure concerns.
Final Thoughts
Many teams try to solve scalability problems by creating more services.
Most of the time the issue is not the number of services.
It is the number of dependencies between them.
Event-Driven Architecture allows modules to collaborate without knowing each other.
When a system stops relying on direct calls for every integration, development speed increases and maintenance becomes significantly easier.
If your UserService sends emails, registers audits, updates metrics, and synchronizes external systems, you probably do not need another service.
You probably need events.
Get new posts by email
No spam — just a note when I publish something new on backend, cloud, and architecture.
One email when a post goes live. Unsubscribe anytime.
Related articles

How to Build a Multi-Tenant SaaS Application in NestJS Without Duplicating Your Code
If onboarding a new customer requires deploying a new application or duplicating an entire database, your SaaS architecture probably isn't ready to scale. Learn how to implement multi-tenancy in NestJS in a clean and maintainable way.

CQRS in NestJS: Stop Mixing Reads and Writes in the Same Service
When your UserService handles POST and GET, optimizations on one side break the other. Learn commands, queries, and handlers with @nestjs/cqrs.

Why Your NestJS Service Becomes a Mess (and How Hexagonal Architecture Fixes It)
Controllers that know too much, entities full of ORM decorators, and tests that need a database. A practical guide to ports and adapters in NestJS.