It all started with a brief from Nuvolar: a small technical test to see how I reason about an existing codebase rather than build one from a blank page. No large systems, no heavy functionality — a flight-plan validation service, most of it already scaffolded, with a clear note on what would be evaluated: the rules implementation, the service and controller wiring, code quality, and the unit tests.
The challenge
The brief described a flight plan — flight number, take-off time, number of passengers, departure and arrival coordinates — and three business rules it had to satisfy to be considered feasible:
- A maximum flight range of 12.000 km, reduced to 8.000 km once the passenger count goes over 150.
- A reduced range of 9.000 km for take-offs after 2:00 p.m., and an outright ban on take-offs between 8:00 p.m. and 6:00 a.m.
- Westbound flights limited to a 3:00 p.m. take-off cut-off and 3.000 km.
And, explicitly, four things left for me to do: implement the validation service, implement the rules, finish the controller, and write the unit tests. The brief also warned that the set of rules "may change in the future" — a hint I took seriously in how I structured the solution.
Reading before writing
Before touching anything, I read what was already there. The domain model (Flight), the request/response DTOs, the mapper, a Haversine distance calculator and a Direction helper for "is this flight going West" were all already implemented and correct. There was also a Rule interface, empty of implementations: validate(Flight) and getErrorMessage(Flight). That interface was the real hint about how the author intended this to be solved.
So I didn't start with the controller, and I didn't start from scratch either. I started by writing the three rules as independent classes against that existing contract, then the service that runs them, and only at the end the controller that exposes it over HTTP.
The decision I weighed the most: one class per rule, not per condition
Rule 2 actually bundles two separate conditions: a range restriction for afternoon take-offs, and a hard curfew between 8:00 p.m. and 6:00 a.m. that applies regardless of distance. I could have split those into two Rule implementations for a stricter one-condition-per-class feel. I chose to keep them together in a single TakeOffTimeRule, because both conditions depend on the same single input — the take-off time — and the brief itself describes them as one rule. Splitting them would have optimized for a principle the brief didn't ask for, at the cost of matching its own structure.
That decision had a second-order consequence worth calling out: what happens when a flight breaks the curfew and would have also exceeded the afternoon range? The rule reports the curfew violation first, since a flight that should never have taken off at all is a more fundamental problem than one that simply flew too far. It's a small judgment call, but it's the kind of thing that's easy to get inconsistent if it isn't decided on purpose.
The other decision that shaped the whole design was letting FlightValidationService depend on List<Rule> instead of naming each rule explicitly. Spring collects every rule bean into that list automatically, so the service never needs to know how many rules exist or what they check. Given that the brief flagged the rule set as likely to change, keeping the service ignorant of the rules felt like the one piece of design effort that would actually pay off later.
Verifying it with tests
The brief asked for unit tests, so each rule got its own test class focused on its exact boundary values — 150 passengers, 14:00, 15:00, and the 20:00–06:00 curfew window — since those boundaries are exactly where an isBefore vs. isAfter mistake would hide. The service was tested twice: once with mocked rules, to prove the aggregation logic in isolation, and once with the real rules wired in together, to catch anything that only shows up when they interact. The controller got its own slice test with the service mocked out, to check the HTTP contract without re-testing the business logic behind it.
As a final check on top of the automated suite, I ran the real application and fired several flight plans at the endpoint by hand: a compliant short hop, an overloaded flight past its reduced range, a night take-off, and a westbound flight past both its time and distance limits. Each one came back with exactly the failing rules it should have — including the case where two rules failed on the very same request.
Leaving it documented
I wrote up the design decisions and the boundary assumptions in a README.md, alongside how to build, run and test the project, so that verifying the result doesn't require re-deriving any of the reasoning above from the code alone.
The result
A validation service built on the contract that was already there, where the rule set can grow without touching the service that runs it, and where the one non-obvious judgment call — how to report two failures at once — is made on purpose instead of by accident.