DEV Community

Cover image for Here is how I would use NestJS to stop one fraud check from freezing an entire fintech platform
Peace Melodi
Peace Melodi

Posted on

Here is how I would use NestJS to stop one fraud check from freezing an entire fintech platform

Picture this. A fintech company processes thousands of transactions a day. Every single one of them has to pass through a fraud check before it goes through. That check calls out to a third party service, or runs some scoring logic that takes a second or two to complete. On a normal day, nobody notices.

Then a busy period hits. Maybe it is a payday, maybe it is a sale, maybe it is just an unusually active morning. Suddenly that same fraud check, the one that took a second or two, starts taking five seconds, then ten. Requests start queueing up behind it. New transactions cannot even get a response back. Support tickets start coming in. Someone on the team eventually says the dreaded sentence, the whole platform is down, and the actual cause is one check that was never supposed to be a bottleneck in the first place.

I have seen this pattern more than once, and it almost always comes from the same root cause. A fraud check, or any external dependency like it, was built to sit directly inside the request response cycle. If I walked into a fintech company and saw this happening, this is exactly how I would think through fixing it using NestJS.

What is actually happening under the hood

The problem usually looks something like this in code. A transaction comes in, and before anything else happens, the controller calls the fraud service and waits for a response before doing anything further.

@Controller('transactions')
export class TransactionsController {
  constructor(private readonly fraudService: FraudService) {}

  @Post()
  async createTransaction(@Body() dto: CreateTransactionDto) {
    const fraudResult = await this.fraudService.checkTransaction(dto);

    if (fraudResult.riskLevel === 'high') {
      throw new BadRequestException('Transaction flagged as high risk');
    }

    return this.transactionsService.process(dto);
  }
}
Enter fullscreen mode Exit fullscreen mode

This looks reasonable at first glance. It even works fine at low traffic. The issue is that every open request is now sitting there waiting on a network call to a service you do not fully control. If that fraud provider slows down even slightly, every request behind it slows down too, and NestJS, like any Node based framework, only has so many resources to keep threads and connections open while everyone waits.

The fix is not to make the fraud check faster. Sometimes you cannot control that. The fix is to stop putting it directly in the critical path of every transaction.

Moving the fraud check off the main request path

Instead of calling the fraud service and blocking the response, I would accept the transaction immediately, mark it as pending review, and hand the actual fraud check off to a background queue. NestJS has solid support for this through its queue integration, commonly paired with BullMQ and Redis.

Here is what the controller looks like once the fraud check is no longer sitting in the request path.

@Controller('transactions')
export class TransactionsController {
  constructor(
    private readonly transactionsService: TransactionsService,
    @InjectQueue('fraud-check') private readonly fraudQueue: Queue,
  ) {}

  @Post()
  async createTransaction(@Body() dto: CreateTransactionDto) {
    const transaction = await this.transactionsService.createPending(dto);

    await this.fraudQueue.add('check', {
      transactionId: transaction.id,
      payload: dto,
    });

    return {
      transactionId: transaction.id,
      status: 'pending_review',
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

The response now comes back almost instantly, no matter how slow the fraud provider is having a bad day. The transaction sits in a pending state, and the fraud check happens separately.

Processing the fraud check in the background

The actual fraud check now runs inside a processor that consumes jobs from that queue. This is where the slow, unpredictable work happens, completely away from the main request thread.

@Processor('fraud-check')
export class FraudCheckProcessor {
  constructor(
    private readonly fraudService: FraudService,
    private readonly transactionsService: TransactionsService,
  ) {}

  @Process('check')
  async handleFraudCheck(job: Job<{ transactionId: string; payload: any }>) {
    const { transactionId, payload } = job.data;

    const result = await this.fraudService.checkTransaction(payload);

    if (result.riskLevel === 'high') {
      await this.transactionsService.markFlagged(transactionId);
    } else {
      await this.transactionsService.markApproved(transactionId);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

If the fraud provider is slow, only this background worker feels it. Requests coming into the platform keep flowing normally. If the provider goes down entirely, BullMQ retry settings let you retry the job a set number of times with a backoff period instead of the whole platform grinding to a halt.

Letting the client know once a decision is made

Since the response is no longer available at the time of the fraud decision, the client needs another way to find out what happened. Depending on what the platform already supports, this is usually done with a webhook, a websocket event, or a status endpoint the client can check.

A simple status endpoint looks like this.

@Controller('transactions')
export class TransactionsStatusController {
  constructor(private readonly transactionsService: TransactionsService) {}

  @Get(':id/status')
  async getStatus(@Param('id') id: string) {
    const transaction = await this.transactionsService.findById(id);
    return {
      transactionId: transaction.id,
      status: transaction.status,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

For platforms that want something more immediate, NestJS also supports WebSockets through gateways, so the client can be pushed a status update the moment the background job finishes, instead of having to poll for it.

Why this matters beyond just fraud checks

The specific problem here was a fraud check, but the same pattern applies to anything slow or unreliable sitting inside a request. Credit checks, KYC verification calls, external scoring services, anything that depends on a third party over the network is a candidate for this same treatment. The moment one of these calls can be slow, the request handling it should not be the one waiting around for it.

The bigger picture

NestJS does not make fraud detection smarter, and it will not fix a slow third party provider on its own. What it gives you is a clean, structured way to separate what needs an instant response from what can safely happen in the background. Queues, processors, and gateways are built in as first class citizens, not something you have to bolt on awkwardly. That structure is what lets a team keep a platform responsive even when the systems it depends on are having a rough day.

If your team is dealing with a similar bottleneck, whether it is fraud checks, KYC calls, or any slow dependency sitting where it should not be, I would be glad to talk through how to restructure it.

I am Peace Melodi, a backend software engineer. If you want your business to scale big, comfortably handling millions of users without breaking, with strong scalability and security in place, feel free to reach out.

LinkedIn: https://www.linkedin.com/in/melodi-peace-406494368
GitHub: https://github.com/PeaceMelodi

Top comments (0)