My company needs to build an application to be rolled out for a lot of clients. This software will have many modules and functionalities, but some of these will need specific adjustments for some companies. Those adjustments might be of minor nature (like a new input in a form), or may be major customizations, like changing the entire form with different steps and business rules before save/edit/delete. It can be expected that the customization may also leads to some changes on database level, like new columns.
The application already exists, but we did not include these changes yet. The application uses .NET, WebApi, AngularJs. As software architecture design pattern we use microservices and a Domain Driven Design (DDD) approach. The Visual Studio Solutions structure is like this:
- WebApp: routes, JS, CSS, html.
- Module-Interface: WebApi Controllers, and Services
- Module-Domain: bounded contexts, aggregation roots, domain.
- Module-Infrastructure: unit of works, repositories, persistence validators, entity mapping
An idea to control this complex scenario is using different branches (on git) for each client. But this may lead to the problem that we need to maintain different applications in the future.
We think it is much better to keep it all in a single branch. But we have a lot of doubts about how to realize it in a good manner (best practice?).
As an example, imagine a CRUD for “User” domain that can have different business rules on different clients. Problems that we see:
-
Do we need to create different save methods on User domain? Like “saveForClient1”, “saveForClient2″…?
- We fear that inheritance on domains could have a negative impact on our DDD approach for this problem.
-
If the customization is that huge that different HTML/Javascript files and .cs files need to be created, but also use already existing code, how can this be organized in a ‘nice’ way?
Updated (18/10/2016)
I changed of job before going more deep in the solution, but there are comments asking about what strategy we choose.
Use different branches it’s the worst idea. The better strategy is control the differences in the code, making different packages, files and REST resource paths to organize this code and attend the specific functionalities. Use interfaces and dependency injection to maintain the same contract in the system but with different implementations for the clients. DDD can help you to isolate this cases where some information from database is client specific.
In the front-end, it’s a good idea to choose a painless library to help you to build interfaces, gaining flexibility to make different interfaces reusing the same components (like React).
6
Customization is as simple or complex as introducing any other requirement. Each time you add a custom business rule or a business rule modification to your application, analyse how to make this rule visible or invisible, active or inactive, available or not available by some kind of run-time switch. Think of the customization step just as an extra, separated requirement, and deal with each of this run-time switches exactly the same as you do with any other requirement: analyse your options how to implement it, make technical decisions, plan the effort, develop, test and deploy the feature. Ask yourself
- which impact will decision have on the existing code base
- which changes will be necessary in your database (maybe you need some custom fields only used for some of your customers, maybe you need a different table, maybe you have to make your model more generic etc.)
Just do not expect a “one size fits all” solution: different business requirements will lead to different forms of customization, so you will have to repeat these steps with each new custom feature. And avoid to group customizations “per client” like “saveForClient1”, “saveForClient2”. Instead, add customizations “per feature” – that gives you a much better granularity. For example, lets assume the business rules in the former case are the validation rules of the save method. You could either have one save method with a customized list of validations before saving happens, you could have a save method with additional parameters for controlling the validation, you could have two different save methods like “saveWithDefaultValidation” and “saveWithSpecialValidation”, you could have a flag in your User object or somewhere else in your data model for controlling which validation rules will apply – and there are many more possible solutions. As an additional option to this, you could also think of a plugin system, where extra functionality is made available by some additional modules sold only to the customers who pay for them (but still your core product remains 100% the same for each customer). Just pick the one which serves your purpose best.
It should be obvious that you will have to plan if your clients shall be able to change the specific customization options by themselves (for example, by some config file), or if you do this for them, or if you need to take some technical measures to prevent your clients for activating features they have not paid for. The plugin system above maybe such a measure. Nevertheless I recommend to avoid different core deployments for different clients, if possible.
EDIT: I like to add that I agree with the OP about not using per-client branches in the VCS for this purpose. The problem which occurs is that whenever a new feature in the trunk is added, the dev who adds the feature cannot see immediately if this will collide with any code of the other branches, and if there is something to be changed, too, or if a more general solution is needed which works for the trunk and client specific releases as well. If you are lucky, you might notice this later, when you try to propagate changes from the trunk to the client branches and get merge collisions. If you are not so lucky, you do not get any merge collisions, but one of your client releases now has a severe bug. Maybe your automatic tests will catch this, maybe your manual testing, or in the worst case the customer is the first who notices it, but it should be clear that your testing effort multiplies with each client. This is because when you allow arbitrary changes in each branch, you will have to make a full integration test with the whole product after a change in the trunk for each branch.
1