Who this is for
This article is for:
- developers new to backend architecture
- people who have heard terms like "event-driven" and "microservices" but want a simpler explanation
- teams building an internal platform and trying not to overcomplicate it too early
PubSub in plain English
PubSub stands for "publish and subscribe."
The idea is simple:
- one part of your system announces that something happened
- other parts of the system can listen for that announcement
- the sender does not need to know exactly who is listening
Think of it like posting a notice on a company board:
- HR posts "New employee joined"
- payroll reads it and sets up salary
- IT reads it and creates accounts
- security reads it and grants building access
HR does not need to call each team one by one. It posts one event, and the right teams react.
That is what PubSub does in software.
Why teams often reach for the wrong solution too early
When engineers hear "event-driven architecture," they often jump straight to advanced tools like Kafka, RabbitMQ, or NATS.
Those tools are powerful, but they also come with real cost:
- more infrastructure to run
- more concepts to learn
- more moving parts in development and testing
- more operational overhead for a team that may not need it yet
For many products, especially early-stage or mid-sized systems, a simple internal event bus is enough.
The goal should not be "use the most impressive tool." The goal should be "solve today's problem clearly and leave room to grow tomorrow."
Why a custom internal PubSub can be a good first step
Inside a monorepo, services already share code, types, and deployment context. That gives you a huge advantage.
You can build a lightweight event layer that gives you:
- faster development, because everything is in one codebase
- shared event contracts, so publishers and subscribers agree on the shape of data
- easier local testing, because you can run a simpler version of the bus in development
- lower cost, because you are not introducing heavy infrastructure too early
- easier observability, because events and handlers live in one place
This does not mean "always build your own tooling." It means "use the simplest tool that solves the real problem."
A practical architecture
In a NestJS monorepo, a small PubSub setup often looks like this:
- shared event contracts in a common package
- a publishing service that sends events
- subscriber classes that react to events
- Redis, a queue, or even an in-memory adapter depending on environment
For local development:
- an in-memory bus can be enough
- it keeps setup simple
- it makes debugging easier
For shared environments:
- Redis PubSub or Redis Streams can be a good middle ground
- you get cross-service communication without jumping all the way to Kafka
The key idea is that your application code should depend on an event interface, not on a specific infrastructure vendor.
Start with clear event contracts
An event is just a message that describes something that already happened.
Good events are:
- specific
- named clearly
- stable over time
- focused on facts, not commands
For example, user.created is better than create-user-for-analytics.
Why?
Because user.created describes a business fact.
Any number of services can react to that fact.
Example contract:
export interface UserCreatedEvent { type: "user.created"; payload: { userId: string; email: string; createdAt: string; }; }
Even if you are new to TypeScript, the important part is easy to understand:
typetells us what happenedpayloadcontains the useful details about that event
Shared contracts reduce confusion because every service reads from the same definition.
A simple publisher
The publisher is the part of the system that announces the event.
@Injectable() export class EventBus { constructor(private readonly redis: Redis) {} async publish(event: BaseEvent) { await this.redis.publish(event.type, JSON.stringify(event)); } }
In plain English, this says:
- take an event object
- convert it into text
- send it to a named channel
The service that creates a user does not need to know who cares about that user. It just publishes the event and moves on.
That loose coupling is the main benefit.
A simple subscriber
The subscriber listens for an event and reacts to it.
@Injectable() export class UserCreatedListener { @OnEvent("user.created") handle(event: UserCreatedEvent) { console.log("User created:", event.payload.userId); } }
In real systems, the listener would do something useful like:
- send a welcome email
- create a billing profile
- record analytics
- start onboarding tasks
The important design benefit is that each listener owns one responsibility. That keeps your system easier to change later.
What beginners often get wrong
1. Turning events into remote procedure calls
Events should describe what happened, not tell another service exactly what to do.
Bad example:
email-service-send-welcome-email
Better example:
user.created
The second version is more flexible and less coupled.
2. Skipping reliability concerns
Even a simple PubSub system needs guardrails:
- what happens if a subscriber crashes?
- can the same event be processed twice?
- how do you know which event failed?
If you skip these questions, the system looks simple at first but becomes hard to trust.
3. Hiding event definitions in random places
If events are not easy to discover, teams create duplicate or conflicting versions.
Put shared contracts somewhere obvious and reusable.
What "good enough" looks like early on
You do not need a giant event platform on day one.
A good early setup usually includes:
- shared event types
- basic logging around publish and consume steps
- retries for temporary failures
- idempotent handlers, so duplicate processing does not break things
- a dead-letter or failure path for events that cannot be handled
That already gets you much further than "we use events" with no operational discipline.
When it is time to move to Kafka or another larger tool
You should consider a more advanced event platform when:
- many independent services need to communicate across multiple repos
- throughput becomes very high
- you need durable replay of old events
- multiple teams need strict ownership boundaries
- message ordering and partitioning become important business concerns
That move should happen because your needs changed, not because bigger infrastructure sounds more professional.
Final advice
Start simple, but not sloppy.
If you build:
- clear event contracts
- observable publish and subscribe flows
- safe handlers
- a clean path to future scaling
then a lightweight internal PubSub layer can be a very strong architectural choice.
The real lesson is not "always build custom PubSub." The real lesson is "match the system design to the stage and complexity of the product."