You could design a rule engine. E.g. you might start with a data model:
class BadgeRule { Condition, Name }
interface Condition { bool MatchesUser(User u); }
class DistanceCondition { MinimumDistance; ... }
badges = [
{ Name = "5km", Condition = DistanceCondition { MinimumDistance = 5'000 } }
{ Name = "10km", Condition = DistanceCondition { MinimumDistance = 10'000 } }
]
But as you want to design more complex rules, you'll likely have to do additional development work to make that information available to the rule engine. Also, there's a risk of running into Greenspun's Tenth Rule, that your rule engine eventually becomes so complex that it's just an ad-hoc programming language.
Instead, you could embed an existing scripting language (e.g. JavaScript, Lua, Python) into your application, allowing new rules to be defined as plugins without having to redeploy the software. This can be quite reasonable and secure, actually (e.g. consider a Wasm sandbox). But it requires that you create suitable bindings between the scripting language and your internal data model. In systems where controlling access to the data isn't important, just letting the plugin define custom SQL queries might be an easy way out.
Many systems don't need this degree of configurability. In particular where the rules are defined by the same organization that creates and runs the software, it could be more appropriate to just modify and re-deploy the software. At the very latest since the popularization of “DevOps”, the software development community now has really good ideas about making deployments fairly painless.
Letting the rule implementation involve coding can also help the new badge go through a Q&A process, to ensure that it works. Testability is often limited in “low-code” systems or rule engines, where new rules might have to be tried out in prod.