My friend Gojko Adzic has been running a series of BDD quizzes illustrating different ways to approach some interesting BDD situations. I noticed on Twitter that Seb Rose, another BDDer (Cucumberer?), had gently taken issue with one of Gokjo’s solutions so I thought I’d take a look at them both. Before reading on I recommend reading Gojko’s solution and Seb’s response for context.
About time for a quiz about time ¶
The action takes place in a payment processor. We want to implement a business rule to cancel an authorised transaction a month after the authorisation date if it hasn’t been charged, to free up reserved funds.
There is a heap of domain terms there so let’s unpack them:
- authorised: the payment is legit and the card company has set aside the funds to honour it
- authorisation date: when it was authorised
- reserved funds: the money set aside
- charged: the payee has requested the funds and they have been paid
- cancel: kill the payment and free up the reserved funds
- a month: this is the detail - we need to understand what a “month” is in this context
Specifying a solution, by example ¶
Gojko’s question was about how to structure the examples to illustrate this scenario. Should you use relative or hard-coded dates? Should you explicitly set the time and then use hard-coded dates relative to that? Or maybe something else?
Spoiler (I told you to read his post first!): Gojko went for hard-coded dates. Plot twist! This was the least popular answer. His reason, which makes a lot of sense, is about distinguishing between acceptance criteria and examples. Acceptance criteria, he argues, are things like “more than a month ago”. Examples are things like “for authorisation date X and processing date Y, the payment state should be cancelled (or authorised)”.
He then has a series of examples as data tables with headings that say “matching date exists”, “end of month”, and then some mad stuff about leap years!
Seb takes issue with this (respectfully. Seb does everything respectfully. Seb is lovely.) His main beef is that the core of the problem isn’t explicit. He introduces a handy acronym for scenarios, BRIEF, that he coined with the equally lovely Gáspár Nagy:
- Business language
- Real data
- Intention revealing
He then applies this to this situation and observes that there are lots of examples. Lots and lots of examples. It reads like a wall of examples. And there are bugs in the examples! He points out that an important twist to the definition of “month” is mentioned only in passing in a comment: “If matching date cannot be calculated, use end of month to cancel.” (He then gets into some weeds about whether to have the year as part of a date or not. I prefer consistency over conciseness because otherwise it throws me off balance.)
Seb comes really close to where I ended up, by noticing that the real crux of this is the definition of “month” and further, that this may be better defined as code-level examples rather than in Gherkin scenarios, but then decides to go with another wall-of-text data table anyway.
Hiding in plain sight ¶
I read both articles and my immediate reaction was:
To me, both blog “scenarios” are really a story about cancellation with several scenarios:
- (The one where) the processing month contains the auth day-of-month
- (…) the processing month is shorter
- (…) It is a leap year
with fewer, clearer examples.
Now I haven’t used Cucumber in a long time—I hardly ever use plain text frameworks for my BDD scenarios—so I didn’t recognise the syntax of a
Scenario Outline with
Examples blocks, which is a kind of scenario generator. I get it but it feels a bit rambling. I prefer a tighter narrative.
In my mind, this is a story, plain and simple, about the business rules for when to cancel an authorised payment that hasn’t been charged. Note: there isn’t anything here about what is involved in cancelling a payment, just about whether it happens.
This, for me, is where the real power of domain-driven design shines in BDD. There is a concept here that is hiding in plain sight and trying to emerge. We want to know when to cancel a payment. That suggests we are missing a cancellation date. Now we can ask a substantively different question:
“What should the cancellation date be for a payment?"
Specifying the real solution, by example ¶
This further suggests that we are missing a cancellation date calculation. The business rule appears to be: The cancellation date is one calendar month after the payment authorisation date. So let’s explore this with some simple examples:
- Authorised on 25 March, cancels on 25 April
- Authorised on 3 September, cancels on 3 October
Based on a naïve definition of “calendar month” we might start with “the same day of month, in the following month”. Next we try to think of an example that breaks this definition. (These are the best examples - these are the ones that guide the design):
- Authorised on 30 January, cancelled on, hmm… let’s think about this.
What if we define cancellation date as “Day of month + Days in month”? So 25 March goes 31 days forward to 25 April, 3 September adds 30 days to land on 3 October. That fits our intuitive examples, and it gives us a reasonable answer for the outliers:
- Authorised on 30 January, cancelled on 1 or 2 March depending on whether it’s a leap year
- 30 Jan 2019 -> 2 Mar 2019
- 30 Jan 2020 -> 1 Mar 2020
That’s handy, all those leap year edge cases just evaporated!
Another definition might be “the same day of month in the following month, or if that doesn’t exist, the last day of the following month”. This gives rise to a number of edge cases, and means we need to explain to payees that payments in January may cancel after 31, 30, 29 or 28 days, and payments on the same date in January or February will have different cancellation periods depending on whether it’s a leap year. I expect they wouldn’t be very happy with that. Of course in reality I would be validating all this with my payments colleagues, and their regulatory friends.
I am struggling to think of an example where our definition of cancellation date wouldn’t make sense. It even rolls over year-ends intuitively.
So now I can write a handful of scenarios for a Cancellation Date Calculation using my trusty “Friends episodes” template:
- The one where the day of month appears in the following month
- The one where the cancellation date rolls into a later month
- The one where it handles February in leap years and regular years
Loose ends ¶
As Seb suggests, this Cancellation Date Calculation is probably just a class - or a function - which we can implement using code-level examples like those above. So it turns out the “definition of month” was really the “definition of cancellation date”, and is very much related to the payment cancellation subdomain! And if the finance folks come back with a trickier definition, we just evolve that using some more examples.
This leaves implementing the Cancellation Processor as a trivial exercise, namely:
describe Cancellation Processor: - it should cancel uncharged payments that have reached their cancellation date - it should ignore uncharged payments that are within their cancellation date
Implementation considerations ¶
We don’t specify whether we calculate the Cancellation Date when the payment is authorised, which means extra storage, or whether we calculate it dynamically when we process the payments, which means extra work at runtime. And we don’t need to. This detail is hidden from both the Cancellation Date Calculation and the Cancellation Processor.
We can model the payment as a state machine, something like this:
[New] -> (authorise) -> [Authorised]
[New] -> (decline) -> [*Declined*]
[Authorised] -> (charge) ->[*Charged*]
[Authorised] -> (cancel) -> [*Cancelled*]
where italics indicate end states. (A real implementation would likely have lots of intermediate Pending and Error states. State machines are tricky.)
So what happened here? ¶
- I noticed lots of similar examples in a scenario (outline). This suggested a hidden rule. It also wasn’t intention-revealing. Tables rarely are.
- There was something that looked like a business rule sitting in a comment, which was easy to miss. (Thanks Seb!) This made me curious.
- Exploring the domain using domain language suggested there was a missing concept, a cancellation date, which in turn suggested the payment processor might be doing too much.
- Once we identified the missing concept, we were able to define a naïve implementation using examples.
- Using more examples, we broke our initial implementation, which guided us to a more comprehensive one.
- We added further examples to illustrate other cases, to increase our confidence in the solution. We made a note to validate our business rule with domain specialists later.
- We observed that our specification still leaves wiggle room for different implementations which would be equally valid, with different performance characteristics.
- We ended up using hard-coded dates, like Gojko, but we needed fewer examples to triangulate on a viable solution because we identified where to separate the concerns.
Postscript - a reality check ¶
There are lots of ways to tackle this kind of problem. My version isn’t “correct” or “better”, it’s just the way I’ve learned to think about it. When a classroom exercise like this hits the real world, things get a lot trickier.
For instance, a more likely definition of cancellation date would be “the first business day on or after the following calendar month," and now you are in the Byzantine world of business day calculations. There are companies whose entire product is a business day calculator! What if the payee and payer are in different territories and 25 April is a public holiday in one and not the other? What if the payer’s bank is in a third territory? What if the payment takes place across time zones, so the payment instant-in-time is on a different date for payer and payee? How do they reconcile this?
There is always more detail to explore in a domain, and always more knowledge to crunch. The balance is in determining when you have a “good enough” solution, which is why examples are so powerful.
And finally, thanks to Gojko for posing a great question, and thanks to him and Seb for inviting me to play.