Say we have an OTP
entity that represents a one-time password that expires after e.g. 15 minutes.
Our system is event-sourced and adheres to DDD principles; for example, we have the following event:
public record OtpCreatedEvent(
string MobilePhoneNumber,
string Password,
IPAddress RequesterIp,
DateTimeOffset OccurredAt
) : IEvent;
Now, upon the creation of new OTPs (e.g. inside a CreateOtp
command), we have two invariants/validations/business rules that need to be satisfied before a new OTP can be created (both of which are set-based):
- A single client (identified by their IP address) cannot create more than 15 OTPs (for any number of phone numbers) within a span of 1 hour.
- At least 20 seconds must’ve elapsed since the last requested OTP for the same phone number.
And in a VerifyOtp
command, we have to retrieve the list of all non-expired (i.e. active) OTPs for the provided phone number, and check the entered password against all of them.
There’s a lot of time-based logic going on. I think broadly speaking, I have 2 approaches I could take:
- Incorporate this logic into the queries that are made to the data store when the commands above are invoked (e.g.
var nonExpiredOtps = dataStore.Otps.Where(otp => otp.CreatedAt > DateTime.Now.AddMinutes(-15))
— massively simplified but you get the idea). - Upon creating a new OTP, somehow “schedule” an
ExpireOtp
command (which will in turn emit anOtpExpiredEvent
) to be invoked 15 minutes from now.
Obviously, approach #2 would require some extra plumbing infrastructure-wise. And approach #1 would result in some awkward-looking code, in some parts.
But assuming that both are doable, I’m curious which approach you’d recommend and why? And if there are any others, I’d like to know.
1