A few weeks ago I was asked by my friend at work, how to make a good abstraction for two classes in Java that looked similar. He was thinking for some time already. I asked him: "Why not simply copy the first one and make a small alteration needed? That will take a few minutes and it will be very readable and easy to change in case there would be a new use case." He said: "I thought about that, but it looked too simple for me".

As Venkat Subramanian said in "The art of simplicity", complexity feels good. When you craft a complex solution you feel like a master in your field. Other people would probably have a hard time understanding it. That feels even nobler!

AEM is a complex platform. There are multiple pieces that need to work together like authoring, publishing, dispatcher. One needs to have multiple skills and a lot of knowledge to make it work properly. If it is already complex in its nature, why add accidental complexity to it?

Have one team

We need System Engineers, Backend Engineers, Frontend Engineers, Quality Assurance Engineers and even more skills to complete an AEM application. Make it simple, let all of those people work in one team and meet daily. Improve the flow of information. Start Dispatcher configuration development on day one. Learn about obstacles at the very beginning of a project. Do not create silos between different skill sets. Allow people to exchange in tasks they do.

Have one code repository

Have one source code repository for the team. They work together, they need to keep consistent coding standards. Have one main README.md where the story begins. If you have a new teammate it would be easier for her to jump in and start the work. Then you can have specific configurations in subdirectories for each part of the system with its specific README.md. You won't need additional wikis, etc. everything in one place, versioned and up to date.

Have one "main" branch

Avoid questions what is the main branch in the project? Yes, you can have feature branches. Just avoid setups with "main" and "develop", where "develop" is like a "main", but somewhat different, and nobody really knows what the difference is.

Integrate with your "main" branch daily

Yes, this is true Continuous Integration. According to Dan North, this practice makes the distinction between outstanding and average teams. Use branching by abstraction and feature toggles to manage the delivery of new features.

Yes, this will make your work simpler. Every teammate will see the code of all other people at least once a day. If you are heading in a totally wrong direction you will get feedback from the review and lose at the most - one day. You will never get into merge hell -> you have at most one day of work on your branch. That is simple but not easy.

Make all your environments equal

Yes, you need to make distinctions between PROD and INT, e.g. you need to connect to two different repositories not to spoil PROD data. However, make the number of differences the bare minimum. Give INT AEM the same amount of memory PROD will have. Don't be cheap, make the setup simple.

The same thing goes to the local environment setup. Never mind if you use VMs or Docker to assemble everything together. Make sure your local setup is a carbon copy of your INT environment.

Automate every repeatable task

If there is a repeatable task out there - automate it.

  1. Every merge to "main" -> automatically update your INT environment with the latest version of the system and run tests.
  2. E2E, unit, functional tests, etc. -> yes, run them on every change. Avoid nightly builds, they just clutter the view. If you want them to verify integrations with external systems, read the next paragraph for a few thoughts about that.
  3. Executing Groovy scripts, APM scripts, etc. -> make them automated and idempotent. Use Gradle AEM Plugin, Chef, anything that would let you remember on a particular instance if it was already run or not.
  4. Stage deployment -> automate it and run tests as well.
  5. Production deployment - yes automate it. Sometimes people think that great things like production deployment need some manual work. Avoid manual tasks especially for production, they are error-prone. When possible, use smoke tests for production.

Make your integrations simple

"Integration with other systems is the most complex part of software delivery" - Damian Mierzwiński, Senior Software Engineer @ Cognifide.

Use contract testing to define integrations. Decide about the contracts between your system and any external system. Immediately write them down using PACTs or any other tool that would let you automate tests. You need to verify the correctness of your system alone. Then run PACTs nightly against all the systems you integrate with and automatically send emails to the system owner in case there are errors.

Don't lose time debugging issues created by other teams. You have enough of your own problems. All may sound complex but believe me, after you will set up your first PACT you will never come back the old way.

Deliver features E2E

Split your delivery feature by feature, not profession by profession. It is not about delivering authoring experience first, then to deliver component look & feel, then Dispatcher configuration to serve resources well. Make all professions work collectively for a single feature. Engage all teammates from start and let them solve the Dispatcher configuration for the very first feature they will deliver. True, it will then evolve when the following features will come to play but that is the nature of it.

You will discover most of the possible risks right away. URL mappings, cache, performance, integrations. All of it will be known for you from the very beginning. Planing people's involvement will be easier: you simply need them all from start!

Stop cutting corners

In one of the last projects I took part in, I was working hard to come up with solid implementation. I wrote unit tests, decomposed code to reduce interdependencies. Sometimes I thought, why are you doing this, you could cut few corners, save a day or two here and there. You could focus on something different then.

I stopped thinking like that when we started to finalize our project. New requirements were unveiled, time pressure was high. At that point, I was able to easily deliver new features and alter existing once thanks to the solid solution I had! I was grateful I put that additional effort earlier - without that, I would be lost at the end.

Conclusions

Don't force yourself to implement every single piece of advice I placed here. No, that won't be simple, and probably won't be possible at all. Pick one or two and try with them. I'm sure it will bring value to the project.

Tom the Builder from Ken Follett's book "The Pillars of the Earth", when asked why he uses the same standard dimensions for different parts of the same cathedral answered: to make building simpler and cheaper via making fewer errors... And because it will look beautiful.

I believe the same goes for software development. The simpler the solution is, the easier to maintain and alter. And when it finally serves the purpose, it becomes simply beautiful.