I feel it is easier to answer the question using a small example:
Example problem: You have a component doing some computation from some input.
Depending on customers, the following things tend to change:
- Source/format of input data
- Pre- processing of input data
- Three different ways [A..C] to compute some output depending on customer use case.
- Output format of computation results.
So the workflow might look a bit like this:
Preprocess -> Compute with either [A..C] flavor -> Format output -> Done.
A design I would consider to handle this without a bunch of configuration spaghetti would be:
- Classify for each processing step a Behavior kind. In the example for the preprocess step, this could be something like IInputShaper, responsible to transform a specific type of input data to the internal format and do some preprocessing. Possibly this could be split up again: IInputFitter, IPreprocess. For the other steps in the workflow, do accordingly. This leaves you with contracts for a number of behavior types (which is also good for understanding your problem domain. You can now easily see where the degrees of freedom actually come from, how many different kinds of behaviors the system has and you can keep the core implementation clean. Also you can test the core code base, you can document the requirements for each behavior contract and you can test the behavior implementations in a unit test fashion rather than "in vivo" - cluttered into the core code base.
- Implement each behavior against the contracts defined in step 1. Accept some duplicate code across the behavior implementations rather than over-designing and trying to factor out commonalities between those behaviors. Keep it simple. And have tests for each of them.
Result: A collection of behavior implementations with 0 configuration options and a core code base with 0 configuration options. Last step is to pick the behavior implementations for the customer project, maybe write a new implementation if there is something special.
If you did this right, you will live happily ever after without changing contracts and core system. If you missed the perfect design by a bit, you will have a few iterations until you figured out the best contract interface design so your core code base becomes stable.
With this approach, you will also have a much easier time to estimate work and write your quotations for new customers. You can simply check in quotation phase, if you have all behavior implementations already or if there are some new ones you need to write. And if you keep track of how long it took to write them in the first place you even will have a good guess for the man hours it will take.