• Fat Cats Boardgame
  • Wicket UI Library
  • About Me

Java and Wicket web development thoughts, tutorials, and tips

roman@coderdreams.com
Coder DreamsCoder Dreams
  • Fat Cats Boardgame
  • Wicket UI Library
  • About Me

How to make your software more configurable

November 8, 2026 Posted by Roman Sery software design customization 0 Comments

A few years ago I wrote about customizing your codebase for each customer using enumerations. The idea was simple: one SiteConfig enum with a default for every setting, and a properties file per customer to override the defaults. It worked, and we still use it for a handful of things. But the application I work on now has well over a thousand configuration options spread across about fifty areas, and the enum approach did not survive that growth. This post is about what replaced it, and what I learned along the way about creating config flags that people can actually use.

The problem with one big enum

Suppose your SiteConfig enum has grown to a few hundred constants. A few things start to hurt:

  • Every value is a string. "yes", "25000" and "NEW,APPROVED" all look the same until something calls getBoolean() on the wrong one.
  • The default is in the enum, but the actual value for customer FB is in site_config_FB.properties. To answer "what does FB get?" you open two files and do the merge in your head.
  • There is no grouping. ORDER_APPROVAL_LIMIT sits next to SSO_ID_ATTR_NAME and TIMEZONE, in whatever order people added them.
  • A change means a deployment. Want to see how the app behaves with a flag turned on? Edit the properties file, build, deploy, wait.

And the worst part didn't live in the enum at all. Because adding an enum constant plus a properties entry was a bit of ceremony, developers took the shortcut: if(Utils.isCUST1() || Utils.isFB()) right there in the business logic. Multiply that by a few hundred and you can no longer tell what a customer's system does without reading all of it.

A better solution: config groups

Let's look at what a configuration area looks like now. It's a plain class, one per feature area, with typed static fields:

@ConfigGroup(module = Module.ORDERS, desc = "Approvals")
@Accessors(fluent = true)
public class OrderApprovalsConfig extends BaseConfig {

    @Configurable(description = "Orders over the limit need a manager's approval before they go to the warehouse")
    @Getter private static boolean requireManagerApprovalOverLimit = isCUST1() || isFB();

    @Configurable(description = "The order total above which a manager's approval is required")
    @Getter private static BigDecimal managerApprovalLimit = isFB() ? new BigDecimal("25000") : new BigDecimal("10000");

    @Configurable(description = "Statuses from which the customer can still cancel an order themselves")
    @Getter private static Set<OrderStatus> customerCancellableStatuses =
            isCUST1() ? Set.of(OrderStatus.NEW) : Set.of(OrderStatus.NEW, OrderStatus.APPROVED);

    //read once at startup when permissions are built, so changing it at runtime would do nothing
    @Configurable(readOnly = true)
    @Getter private static Group approverGroup = isFB() ? Group.FINANCE : Group.MANAGER;
}

There are a few things we should note here:

  • The values are typed. A boolean is a boolean, a limit is a BigDecimal, a set of statuses is a Set<OrderStatus>. No parsing anywhere.
  • The default is a Java expression, so it can be computed from the customer code when the class loads. The customer checks still exist, but they live in exactly one place: the config class.
  • @ConfigGroup says which module the group belongs to and gives it a readable name. @Configurable adds a description in plain English and can mark a field as read-only.
  • The business code never mentions a customer:
if(OrderApprovalsConfig.requireManagerApprovalOverLimit()
        && order.getTotal().compareTo(OrderApprovalsConfig.managerApprovalLimit()) > 0) {
    createApprovalTask(order);
}

That last point is the whole reason to do this. Compare it to if((isCUST1() || isFB()) && order.getTotal().compareTo(limitFor(customer)) > 0). The new version tells you what the rule is. When a third customer wants the same behavior you change one line in the config class and no business logic at all.

If you read part 2 of the customization series, you may wonder how this relates to conditional beans. They solve different problems. A config flag is for a yes/no or pick-one decision inside otherwise shared logic. When the logic itself diverges, a separate implementation chosen by a condition is still the right tool. Don't try to express a different workflow as twelve booleans.

Make them visible

Once you have more than a hundred or so options, nobody remembers them. So the next step was a page that lists every one of them. A classpath scan finds every class annotated with @ConfigGroup, reflection reads every static field, and the page shows group, key, description and current value, with a search box and a group filter:

Reflections reflections = new Reflections("com.myapp");
for(Class<?> group : reflections.getTypesAnnotatedWith(ConfigGroup.class)) {
    for(Field f : group.getDeclaredFields()) {
        if(Modifier.isStatic(f.getModifiers()) && !f.isSynthetic()) {
            f.setAccessible(true);
            rows.add(new ConfigRow(group, f.getName(), f.get(null)));
        }
    }
}
The config options page, filtered to one group

That's about all the magic there is. But the effect on day to day work was bigger than I expected. "Why does customer FB get an approval task and CUST1 doesn't?" used to be a code reading exercise. Now it's a search on a page, and people who don't read Java can do it too.

Override at runtime

Since the fields are static but not final, they can also be set through reflection. So the page grew an edit button. An override is saved to a small table (group class, key, new value, who changed it and when) and the edit modal adapts to the type: a yes/no radio for booleans, a dropdown for enums, a multi-select for a set of enums, a validated text box for numbers.

Editing a boolean config key

Let's take a closer look at the details that turned out to matter:

  • Saved is not applied. Overrides are saved as pending and take effect on an "Apply Changes" button, so you can change five related flags and switch them on together.
  • The table is the desired state, not a list of edits. On apply, every editable field is set to its override if it has one, otherwise back to the original value captured at startup. Removing an override therefore reverts the field on every server, not just the one you clicked on.
  • Clusters. We run several nodes. A version value in the database is bumped on apply, and each node checks it every couple of minutes and reloads the full state when it changed. No messaging, no cache invalidation framework, and it has been boring in the best way.
  • Invalid overrides don't win. If an enum constant was renamed since the override was saved, the field keeps its default and the page shows a red "invalid value" label instead of silently applying garbage.

Here is what that looks like. A saved override is flagged until it's applied, along with who changed it and when:

A saved override waiting for Apply Changes

After "Apply Changes" the flag goes away and the page confirms that the other servers will follow shortly:

The override in effect after Apply Changes

And because the table is the desired state, removing an override is just another pending change. The row keeps showing its default and warns you that the running value will revert on the next apply:

A removed override, still in effect until the next apply

Export, import and compare

With overrides came the obvious question: "can you make my environment look like FB's production?" So the page can export every option to a CSV (group, class, key, default, whether it's overridden, and the override), and import that CSV somewhere else. Import treats the file as the desired state of every field it lists, validates the whole file first, and saves all or nothing.

Compare is the one I use the most. Upload an export from another environment and you get a diff: different, only here, only in file (the last two are usually options added or removed between releases). For list values the items that differ are highlighted individually, and you can tick the rows you want and copy just those values into this environment.

Comparing this environment with a CSV exported from customer FB's production

Imagine answering "what's different between their UAT and their production?" with one upload instead of an afternoon of grepping.

The zero-config customer

I've saved one of the biggest reasons for doing it this way for last. Not every customer can afford custom development. Some have a much smaller budget, and the custom panels, workflows and integrations that the bigger customers pay for are simply out of reach for them.

Config flags change that math. Every flag that was ever added for a big customer is a feature a small customer can have too, because it is already built, tested and in the release. Turning it on is one line in a config class, or a few clicks on the page, not a project with an estimate. Suppose requireManagerApprovalOverLimit was built and paid for by FB. When a new customer asks "can orders over a certain amount go to a manager first?", the answer is yes, today, at no extra cost. The more customers you have, the longer that list of free options gets.

To make this work, all the customers without custom development run under one shared customer code. Each config group has a small method that sets the defaults for that code at startup, so the "standard edition" is defined explicitly, group by group, instead of falling out of a pile of !isCUST1() && !isFB() expressions:

public static void setDefaultsForStandard() {
    requireManagerApprovalOverLimit = true;
    managerApprovalLimit = new BigDecimal("10000");
    approverGroup = Group.MANAGER;
}

A nice side effect is that every new flag now comes with one more question: what should the standard edition get? Before, that question had nowhere to live.

How to create good config flags

Here is where I have opinions 馃檪

Name the behavior, not the customer. requireManagerApprovalOverLimit, never fbMode. The default expression may mention customers, the name and the callers never do.

Pick the narrowest type. A boolean if it's on/off, an enum if it's pick-one, a set of enums if it's pick-many, a number if it's a limit. A string that gets parsed later is a flag that will break later.

One flag, one decision. If turning a flag on changes three unrelated things, it's three flags. And if flag B only makes sense when flag A is on, compute that in the config class (showApprovalColumn = requireManagerApprovalOverLimit && ...) instead of checking both everywhere.

Write the description for the person who didn't write the flag. Six months from now that person is you. "Orders over the limit need a manager's approval" beats a camelCase name of forty characters.

Mark what can't change at runtime. Anything read once at startup, such as values used to build permissions or scheduled jobs, should be read-only on the page. Otherwise someone will flip it, see nothing happen, and lose trust in the whole mechanism.

Group with prefixes. Within a group, student_AllowedToBeOwner and student_RequiresAdvisor sort together and read as a family.

Delete flags that have converged. When every customer has the same value and nobody asks for the other one, inline it. A flag nobody flips is just noise on the page. Leave a TODO with the release after which it can go.

When not to bother

If you have one customer and one environment, a properties file is fine and this is over-engineering. The right time to build the page is when you catch yourself reading code to answer "is this on for them?". The right time to build runtime overrides is when you catch yourself deploying to find out.

Conclusion

Configurability isn't a flag count. It's whether someone can see what the system does for a given customer, change it safely, and move that configuration between environments without a developer in the loop. Typed config groups, a page that lists them, and a way to diff two environments got us most of the way there.

If you've solved this differently, or have config flag naming rules I missed, let me know in the comments!

0 Comments
Share
0

About Roman Sery

I've been a software developer for over 10 years and still loving Java!

You also might be interested in

Customizing your codebase for each customer using Enumerations

Jul 26, 2019

Use enums and properties files to define all the behaviors of your application that are available for customization to your clients.

Customizing your codebase per customer: Part 2 – Conditions

Apr 25, 2020

In part 2 we look at a more powerful approach to customizing your codebase.

Please dont use client-specific release branches

Aug 25, 2020

Why you should avoid a release strategy involving client-specific branches.

Categories

  • aws
  • customization
  • database
  • debugging
  • enum
  • java
  • models
  • performance
  • projects
  • react
  • software design
  • Spring
  • tool
  • Uncategorized
  • wicket

Recent Posts

  • How to make your software more configurable
  • Rent Day
  • Self-contained Wicket Fragments
  • Pros and cons of unit testing
  • Themeable React Monopoly board

Recent Comments

  • TCI Express Thanks for sharing such insightful information. TCI Express truly stands out as the best air logistics company, offering fast, secure, and efficient air express and cold chain transportation services....

    Tracking down a bug in production Wicket application 路  March 25, 2025

  • Tom Error: A zip file cannot include itself Can you please correct the plugin part so it doesn't use the same folder as input?

    Deploying Spring Boot app to AWS Beanstalk with Nginx customization 路  September 3, 2021

  • Golfman: Reality always wins I've used both Wicket and front-end JS frameworks and, having worked extensively on both, I can tell you that "Speed of development" is definitely NOT with the JS frameworks. You basically end up...

    Five reasons you should use Apache Wicket 路  August 29, 2021

  • Kiriller Sorry can not agree with you, wicket might be a well built technical framework. But the advantages of using a front-end framework like react.js and vue.js can not be beaten by Wicket nowadays. - Speed...

    Five reasons you should use Apache Wicket 路  August 23, 2021

  • Bernd Lauert Sorry but i have to refute your claims with the following arguments: 1. the Wicket community may be small but it is also very responsive, you usually get a helpful answer from the core devs on the...

    Five reasons you should use Apache Wicket 路  July 1, 2021

Archives

  • November 2026
  • May 2021
  • October 2020
  • September 2020
  • August 2020
  • July 2020
  • June 2020
  • May 2020
  • April 2020
  • March 2020
  • February 2020
  • January 2020
  • December 2019
  • November 2019
  • October 2019
  • September 2019
  • August 2019
  • July 2019

Contact Me

Send Message
Prev Next