Account Service Microservice with NestJS
What the Account Service Actually Does
The account-service is not a full CRUD service. It’s a lightweight event listener. Its only job: receive account-created events from the transaction-service via RabbitMQ and log them. The transaction-service handles the HTTP endpoint and emits the event; the account-service reacts to it.
This separation means account creation is asynchronous — the HTTP response returns before the account-service has processed the event.
The Event Flow
POST /account (HTTP)
│
▼
transaction-service
│ emit("account-created", account)
▼
RabbitMQ (account_queue, durable)
│
▼
account-service
│ @MessagePattern("account-created")
▼
console.log(account)
The Producer: Transaction Service
The transaction-service exposes the HTTP endpoint and emits to RabbitMQ:
// transaction-service/src/app.controller.ts
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
@Inject(ACCOUNT_SERVICE) private readonly accountRMQClient: ClientProxy
) {
this.accountRMQClient.status.subscribe(status => {
console.log('RMQ Client Status:', status);
});
}
@Post('account')
async createAccount(@Body() account: any) {
try {
await lastValueFrom(this.accountRMQClient.emit("account-created", account));
console.log("Account emitted successfully:", account);
} catch (err) {
console.error("Failed to emit to RMQ:", err);
}
}
}
A few things to note:
@Inject(ACCOUNT_SERVICE)— theClientProxyis a NestJS abstraction over the RabbitMQ connection.ACCOUNT_SERVICEis just a string constant ("ACCOUNT_SERVICE") used as the injection token.emit()vssend()—emit()is fire-and-forget (no response expected).send()expects a response from the consumer. Account creation is one-way, soemit()is correct.lastValueFrom()—emit()returns an Observable. Wrapping it inlastValueFrom()converts it to a Promise, soawaitactually waits for the message to be dispatched to RabbitMQ. Without this, thetry/catchwouldn’t catch RabbitMQ connection failures.
Module Registration: RabbitMQ Client
The RabbitMQ client is configured in the transaction-service’s module:
// transaction-service/src/app.module.ts
@Module({
imports: [
ClientsModule.register([
{
name: ACCOUNT_SERVICE,
transport: Transport.RMQ,
options: {
urls: [`amqp://guest:guest@${fs.existsSync('/.dockerenv')
? 'host.docker.internal' : 'localhost'}:5672`],
queue: 'account_queue',
queueOptions: { durable: true },
},
},
]),
],
controllers: [AppController],
providers: [AppService, ConsulService],
})
export class AppModule {}
The fs.existsSync('/.dockerenv') check at module initialization swaps localhost for host.docker.internal when running in Docker. durable: true means the queue survives RabbitMQ restarts — messages aren’t lost if the broker bounces.
The Consumer: Account Service
The account-service bootstrap connects both an HTTP server and a RabbitMQ microservice transport:
// account-service/src/main.ts
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
const isDocker = fs.existsSync('/.dockerenv');
const rabbitHost = isDocker ? 'host.docker.internal' : 'localhost';
const rabbitMQUrl = process.env.RABBITMQ_URL || `amqp://guest:guest@${rabbitHost}:5672`;
const httpPort = 3003;
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.RMQ,
options: {
urls: [rabbitMQUrl],
queue: 'account_queue',
queueOptions: { durable: true },
},
});
await app.startAllMicroservices();
await app.listen(httpPort, '0.0.0.0');
}
Two transports, one NestJS app:
- HTTP on port 3003 — serves the
/healthendpoint for readiness checks - RabbitMQ — listens on
account_queuefor incoming messages
The actual message handler is minimal:
// account-service/src/app.controller.ts
@Controller()
export class AppController {
@Get('health')
health(): string {
return 'OK';
}
@MessagePattern("account-created")
handleAccountCreated(@Payload() account: any) {
console.log('[Account-Service]: Received account: ', account);
}
}
@MessagePattern("account-created") tells NestJS to route any RabbitMQ message with that pattern to this handler. The @Payload() decorator extracts the message body. Right now it just logs — in a production system, this is where you’d persist the account to a database.
Why RabbitMQ (Not Kafka) Here
PFMS uses both Kafka and RabbitMQ. The choice for account creation is deliberate:
- RabbitMQ is used here because account creation is point-to-point: one producer (transaction-service), one consumer (account-service). RabbitMQ’s queue model is a natural fit — messages sit in a durable queue until the consumer acknowledges them.
- Kafka is used elsewhere (budget-service → notification-service) because budget events are fan-out: multiple consumers might subscribe (notifications, analytics, auditing). Kafka’s topic model supports multiple consumer groups natively.
Using both adds operational overhead (two broker systems to maintain), but each pattern gets the right tool.
Summary
- Lightweight event listener — the account-service has no CRUD, just
@MessagePattern lastValueFrom(emit())— converts Observable to Promise for proper error handling- Dual transport — HTTP for health checks, RabbitMQ for event consumption
durable: truequeues — messages survive broker restarts- RabbitMQ for point-to-point — right tool for single-consumer patterns