How to Ensure Atomicity in Multi-Step Booking Cancellation with Email Notification in Next.js
Unanswered
Rex posted this in #help-forum
RexOP
Hello :)), I'm trying to to 2 different requests in a route.ts and I want both to be successfull or fail and rollback the database.
6 Replies
RexOP
Harrier
I guess that the question is about the db part, since atomicity is related to it. The best solution is to wrap these oprations in a transaction. This way either two of them succeed or fail and rollback.
RexOP
thanks, it can be done also if I'm doing different types of request, one of them not related to db?
because I'm doing one on db, and another one to send email
Harrier
I see that in your case you're using resend, which I guess call their API and they're responsible for handling email sending.
If you're asking if all of the operations can be atomic - both operating on DB and sending an email - I doubt.
While I haven't implemented such things, I've seen in bigger software such as Jira, that the mails land in the queue and the mail queue is flushed on interval. I guess this is handled by the resend itself, so I woudldn't worry about it.
But since it's an external service to yours - it can be down, overloaded, you can hit rate limit etc etc, it would probably be a good idea to put this request into a queue and let the worker pickup the task from queue and handle it. That's how I would approach it today, but I don't know the implementation details.
No matter if you're using a Vercel or not, I would look at message queues, and see how it can be implemented if you want to decouple it. Otherwise, for reasons mentioned, it could slow down your request.
If you're asking if all of the operations can be atomic - both operating on DB and sending an email - I doubt.
While I haven't implemented such things, I've seen in bigger software such as Jira, that the mails land in the queue and the mail queue is flushed on interval. I guess this is handled by the resend itself, so I woudldn't worry about it.
But since it's an external service to yours - it can be down, overloaded, you can hit rate limit etc etc, it would probably be a good idea to put this request into a queue and let the worker pickup the task from queue and handle it. That's how I would approach it today, but I don't know the implementation details.
No matter if you're using a Vercel or not, I would look at message queues, and see how it can be implemented if you want to decouple it. Otherwise, for reasons mentioned, it could slow down your request.
Harrier
One more thing - I know you didn't ask for it, but this function is quite lengthy. Most of the time instead of commenting what the code does, you could instead extract it into a well named function.
Take a sendEmail for example - if you were to change the email provider to some other, youre tighly coupling your code to this particular resend lib. Instead you can introduce a small abstraction, that makes more sense from the perspective of your system:
Abstractions aren't bad, but have to be used sparingly.
Take a sendEmail for example - if you were to change the email provider to some other, youre tighly coupling your code to this particular resend lib. Instead you can introduce a small abstraction, that makes more sense from the perspective of your system:
// @/features/mail/mail.ts
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendEmail({
to,
subject,
template,
text = "",
}: {
to: string[];
subject: string;
template: React.ReactElement;
text?: string;
}) {
const response = await resend.emails.send({
from: "disdette" + process.env.RESEND_FROM_EMAIL,
to,
subject,
react: template,
text,
});
if (response.error) {
throw new Error(`Failed to send email: ${response.error}`);
}
}
// route.ts
import { sendEmail } from "@/features/mail/mail.ts";
type BookingCancellationData = {
studentId: string;
lessonType: LessonType;
day: string;
startTime: string;
endTime: string;
subjectName: string;
tutorEmail: string;
};
async function sendCancellationEmail(data: BookingCancellationData) {
const lessonTypeFormatted =
data.lessonType === LessonType.GROUP ? "di Gruppo" : "Individuale";
await sendEmail({
to: [data.tutorEmail],
subject: "SmartScholars",
template: EmailCancelTemplate({
lessonType: lessonTypeFormatted,
day: data.day,
startTime: data.startTime,
endTime: data.endTime,
subjectName: data.subjectName,
}),
});
}
Abstractions aren't bad, but have to be used sparingly.