Making Code
Back to blog
Architecture4 min read

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.

Your API Doesn't Need More Services, It Needs Events

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