Back to Blog
February 10, 20266 min read

Event-Driven Systems in a NestJS Monorepo

This post explains what event-driven systems are, why teams use them, how they fit inside a NestJS monorepo, and which design habits keep them understandable instead of chaotic.

ArchitectureNestJSBackend

Who this is for

This article is for readers who:

  • are new to backend systems
  • keep hearing terms like "events," "queues," and "loose coupling"
  • want to understand the idea before diving into complex infrastructure

What event-driven means in plain language

An event-driven system is a system where parts of the application react to things that happen.

Examples of events:

  • a user signs up
  • an order is placed
  • a payment fails
  • a file upload completes

Instead of one service calling every other service directly, it can emit an event like user.signed_up. Other services that care about that event can listen and respond.

This is useful because it reduces direct dependencies between services.

In plain English:

  • the signup service does not need to know how email works
  • the email service does not need to know how billing works
  • each part focuses on its own job

Why teams like event-driven design

When used well, event-driven systems help with:

  • separation of responsibilities
  • easier scaling of busy workloads
  • better resilience when one service is temporarily slow
  • easier extension of the platform over time

For example, after a new user signs up, you might want to:

  • send a welcome email
  • create a billing customer
  • record analytics
  • notify a support dashboard

If the signup service does all of that itself, it becomes bloated and fragile. If it emits one event and lets other services react, the design stays cleaner.


Why a monorepo makes this easier

A monorepo means multiple services live in one repository.

That can make event-driven design easier because:

  • contracts can be shared in one common package
  • services can reuse the same types
  • teams can find event definitions quickly
  • local development is usually simpler

This does not remove all complexity, but it removes a lot of accidental complexity.

Instead of asking, "Where is the schema for this event?" you can keep it in one obvious place.


The core principles that matter most

1. Events are contracts

An event is not just a message. It is an agreement between the publisher and every subscriber.

That means the event name and payload shape matter.

If you change an event carelessly, you can break other services without realizing it.

2. Services should be loosely coupled

Loose coupling means one service should not need deep knowledge of another service's internal logic.

The publisher says, "This happened." Subscribers decide what to do with that information.

3. Observability is required, not optional

If you cannot trace what happened to an event, you will struggle in production.

You need to know:

  • when it was published
  • which service processed it
  • whether processing succeeded
  • whether it was retried or failed

4. Handlers should be idempotent

Idempotent means running the same event more than once should not create duplicate damage.

This matters because distributed systems sometimes retry messages.

If the same payment.completed event arrives twice, your system should not charge the customer twice.


A simple mental model

Think of the system in three steps:

  1. Something happens in the business domain.
  2. A service publishes an event about that fact.
  3. Other services react independently.

That is the full idea.

The implementation can become advanced later, but the mental model should stay simple.


Example flow

Here is a beginner-friendly version of an event flow:

  1. A user creates an account.
  2. The auth service emits user.created.
  3. The billing service listens and creates a billing record.
  4. The email service listens and sends a welcome email.
  5. The analytics service listens and records the signup.

The important point is that the auth service does not manually coordinate all the follow-up work. It announces the event once.


Where queues fit in

A queue is a tool that stores work to be processed.

Queues are helpful when:

  • work might take time
  • you do not want to block the user request
  • you want retries
  • you want to process jobs in the background

Example:

@Processor("email") export class EmailProcessor { @Process("sendWelcome") async handle(job: Job) { // send email } }

You do not need to know every NestJS detail to understand this. This code simply means:

  • there is an email work queue
  • a worker watches that queue
  • when a "sendWelcome" job appears, the worker handles it

That is useful because sending email should usually happen in the background, not inside the user's request path.


Events vs direct service calls

Not everything should become an event.

Direct calls are often better when:

  • you need an immediate answer
  • the action is part of one tight request flow
  • failure must be reported right away to the caller

Events are often better when:

  • the reaction can happen later
  • multiple systems care about the same fact
  • you want to reduce direct dependencies

Good architecture is not choosing one pattern forever. It is choosing the right pattern for the job.


Observability for beginners

Observability sounds intimidating, but the basic idea is simple:

"Can we understand what the system is doing?"

In event-driven systems, that usually means:

  • structured logs so machines and humans can search events clearly
  • metrics so you can count failures, retries, and throughput
  • tracing so you can follow one request or event across services

Common tools include:

  • OpenTelemetry
  • structured logging libraries
  • dashboards and alerts

You do not need every tool on day one, but you do need enough visibility to answer, "What happened to this event?"


Common beginner mistakes

1. Creating too many vague events

Names like updated or processed are too vague.

Prefer clear names like:

  • invoice.created
  • user.email_verified
  • payment.failed

2. Mixing business facts with technical commands

Business facts are easier to reuse and reason about.

3. Forgetting failure paths

If a subscriber fails, what happens next?

  • retry?
  • alert?
  • dead-letter queue?
  • manual review?

Ignoring this question creates hidden fragility.

4. Making every feature asynchronous

Sometimes a normal API call is simpler and better. Do not turn event-driven architecture into a fashion statement.


A healthy starting point in a NestJS monorepo

If you are just getting started, focus on:

  • one shared package for event contracts
  • clear event names
  • background workers for slow jobs
  • idempotent handlers
  • basic tracing and logs

That combination is already enough to build a strong foundation.


Final thoughts

Event-driven systems are not really about queues. They are about designing software so that change is easier to absorb.

If you keep the contracts clear, the responsibilities separated, and the flows observable, event-driven architecture can make a growing platform much easier to evolve.

If you skip those basics, the same pattern can quickly turn into confusion.