Technical Approach

This describes in some detail the general approach we have adopted for all of our software development. This includes technologies, tooling, infrastructure, architecture, design, implementation and testing.
Approach

Fundamentally, all code should be highly readable. We aim for readability over all other considerations. Further, we reject code written in a non-obvious manner. There may be a (limited) number of occasions when performance is the key driver, however every effort should still be made to maintain overall code readability, regardless.

Additionally, 'Dependency Injection' should always be used for the handling of external dependencies.

Highly readable code, written with obvious purpose and intent, whose external dependencies are injected, is easy to test and easy to verify.

However the purpose and intent of the code must also be explicitly stated; preferably using embedded documentation (depending on the technology in use, eg JavaDocs). This documentation is particularly useful within IDE's. Auto-completion (again within IDE's) is also extremely helpful when working with such code.

Whilst it should be possible to read a block of code and quickly ascertain both its meaning and accuracy, comprehensive unit tests must also be used to provide confirmation of expected behaviour. These tests also serve as extremely helpful usage examples; and should be read as such, as part of the code documentation.

To prevent duplication of effort, we endorse wide-spread software reuse, by default. All code should be written with future reuse in mind. This includes both infrastructure code and business logic.

There is a common misconception that creating reusable software significantly increases the amount of development time required. But actually this only holds true if you are doing it wrong. Applying solid software development best-practices and technical craftsmanship in a very specific manner can deliver software which is both high-quality and amenable to reuse, within an agreeable timeframe.

Most non-reusable code can be made reusable by extracting all elements (eg limitations, constraints and assumptions) specific to a particular implementation. Such extracted elements should be made configurable.

Being highly configurable is a key characteristic of reusable software.

Rigid adherence to the DRY principle (don't-repeat-yourself) is also mandated.

Aspect Oriented Programming (AOP) should be considered as an additional mechanism for both the avoidance of duplication and the advancement of reuse.

One particularly helpful usage of AOP is with the standardisation of logging. Standard logging formats should be carefully designed and should (where relevant) include correlation identifiers. Standardised logging formats permit a range of useful tools to be built, to scrape the log files, offering both monitoring support as well as 'Business Intelligence' capability.

Deeply embedded security is another area where AOP is extremely beneficial; ensuring security is applied at appropriate levels throughout the code hierarchy, rather than merely superficially at the surface.

We encourage wide-spread process automation. All manual steps should be minimised, with an "automation first" approach. Of course all automation scripts must be treated as first-class citizens; coded with reuse in mind and adhering to the same design principles just discussed. Specifically, scripts should be comprehensively documented and exhaustively tested, with all data entry fully validated and suitable error handling applied.

Whenever someone suggests 'putting something on the wiki', we suggest a better alternative would be documenting those technical steps within a script. Such a script can be run, tested and placed under source-control. However on the corporate wiki, the documentation will stagnate, as few will ever know it exists nor be able to find it easily if they do. It will also quickly become out-of-date.

We prefer runnable, testable code over reams of documentation.

Reusable automation scripts help improve standardisation across projects; assisting with configuration, builds, deployments, environment setup and DevOps tasks. In particular, the creation and maintenance of immutable environments (which we also recommend) demands automation.

Technologies

In terms of tech-stack though, the actual implementation technologies are (perhaps strangely) completely irrelevant. It may seem like an unusual statement for a developer to make, but in reality the implementation is of little importance. It is the underlying ideas which are key. Provided those ideas are fundamentally honoured, the exact nature of implementation matters little.

Therefore feel free to use your preferred languages, technologies, frameworks, tools and assorted utilities.

Having said that, we have based our implementations around Java, AspectJ and a little Groovy, coupled with a number of shell scripts; all running on GNU/Linux (we prefer the systemd free 'Devuan' distribution). Your tech-stack could be completely different though; and that is still ok.

But craftsmanship needs to be the key driver, and should be the goal to which we all continually strive.

Tooling

Choose your own set of development tools. The only important thing is that you fully understand how to use your chosen toolset. So ensure you learn how to use each of your tools effectively and efficiently, and keep them up-to-date.

Becoming a master of the tools you use is an absolute must. So invest the time to ensure you understand all of the settings, configuration options, menu items, widgets and keyboard short-cuts, as well as the range of available plugins.

There are several good Integrated-Development-Environments. We favour Eclipse. We have tried the majority of the alternatives (both free and commercial), each with their own quirks and idiosyncrasies. Whilst they all have some annoyances, we feel Eclipse annoys us the least. You should choose your own preference though.

Regardless of implementation language being used, we encourage the majority of the compiler warnings to be enabled. Many of these are disabled by default, but turning (most of) them on will help identify many easily missed issues. The compiler is really your best friend, so give it every opportunity to tell you about anything which looks suspicious. Certainly experience will dictate which of these warnings within your code-base need to be addressed, but awareness is key.

Static code analysis tools should also be used as a matter of course. However the configuration of such tools should be carefully considered and tweaked as required. The out-of-the-box settings have likely been created by some corporate entity and the resultant checks may not reflect your needs or preferred coding style. Do not hesitate to modify these settings, but do so for good reasons only and document 'what' you have changed and more importantly 'why'. Then ensure you back-up your configurations, as they could easily be clobbered by future updates.

More importantly, actually go and fix the issues reported by these tools. Do not ignore the problems. At the very least, investigate each issue (yes, even the inconvenient ones) and fix them. If there is something you have no intention of fixing (presumably for some very specific and well considered reason), then either add a flag to the code to suppress that issue within that particular block or modify the tool configuration to suppress it globally, otherwise legitimate issues can easily get lost within the noise.

We use 'checkstyle', 'PMD' and 'SpotBugs' (successor of 'FindBugs') along with (mostly) enabled compiler warnings. We strive to maintain clean builds. You should too.

It should go without saying that use of Source Control is mandated. Distributed Source Control systems are favoured. However, as with all aspects of software development, we recommend restraint and resistance to jumping on the latest band-wagon which happens to be passing by.

'Git' is certainly the current poster-boy within this space; custom designed and built for use by the Linux kernel developers. It is most definitely a powerful tool. But we would argue that unless you actually happen to be involved in maintaining the Linux kernel, then it is precisely the wrong tool to use.

Other, very capable tools exist and should be considered, with due-diligence applied before justified selection. Personally, we highly recommend 'Mercurial'; rock-solid and easy to use. Whilst it may not be applicable for every organisation, all alternatives should be thoughtfully evaluated.

CI/CD pipelines are also encouraged, with a range of tooling options being readily available. We really have no preference, but in general, Jenkins does a pretty decent job for us. For cloud-based offerings, GitHub 'Actions' or BitBucket 'Pipelines' also work well.

Finally, we prefer (light-weight) issue tracking tools. Specific tools are likely mandated by your organisation, but we are drawn to the simplicity and corresponding elegance of "Trac". It fully satisfies our needs without getting in the way. Within corporate environments however, enterprise tools such as "Jira" often form the tools of choice. From our perspective, provided such tools are light-weight in nature and allow you to get your job done with the least hassle or ceremony, then they are good enough.

We have much to say about (so called) 'methodologies', suffice to say that, whilst we have no issue with the Agile Manifesto nor with the ideas or intent behind it, we take great issue with the entire industry which has smothered the (now corrupted) Agile approach.

Application Type

High-quality software, which is amenable to reuse, is applicable regardless of the intended system architecture or application type. It is an equal fit for backend batch processing, traditional web-apps, cloud-based micro-services, mobile applications or command-line utilities.

Testing

Unfortunately within our industry the majority of applications fail to be comprehensively tested. This is to be expected given the typical pressures on development time, the way the business logic seeps into the infrastructure code and the myriad of pathways existing within the average application.

Since our approach provides generous opportunities to make time savings and since our clean, modular code-base intentionally omits typical defensive checks, pushes exception handling up to the higher levels and encourages tight data validation at application boundaries, testing becomes more streamlined as the code becomes significantly more linear.

Also, components exporting a common interface can be swapped for more basic implementations; possibly providing canned data to verify the logic of standalone components, permitting testing in isolation.

It is absolutely imperative that generic, reusable code modules are comprehensively tested since they could form the core of multiple applications. The approach is to build it once, test it thoroughly and reuse it extensively.

Time savings should then be re-invested to ensure the completeness of test coverage. Only tried-and-tested code can become a candidate for reuse, since only a high level of testing can provide a measure of confidence in the underlying code-base.

Deployments and Environments

Within many software development shops, deploying software is a major source of stress and hassle with a significant increase in the team workload as the release date nears. Given the whole point of developing software is to get it into the hands of the users who need it, you would think by now we would have developed much easier ways to complete this task. It does not need to be a difficult, time-consuming and stressful exercise.

One of the major headaches concerns the management of environments.

Typically there will be at least 3 deployment environments; namely development, test and production. There may also be additional environments in use such as system test, user test, performance test or production mirror etc.

The main issue seems to be that none of these environments are kept totally in sync with the others. Once we enter a situation where there are differences between environments, all bets are off in terms of properly testing the software.

What is it we are actually testing? We are testing the behaviour and interaction between the input data, the software product and the deployment environment into which it is placed. Therefore it makes no sense whatsoever to test the software in environment A, then move it into environment B and expect identical outcomes if environment B is not exactly identical to environment A. Why would any right-minded individual expect the same outcome under different circumstances?

The fundamental reason why environments get out of sync is simply due to the application of manual updates. The answer is therefore painfully obvious: no manual updates should be made to any deployment environment - ever!

It is of course completely acceptable to experiment with setup and configuration steps in a development environment; after all that is what it is for. But once we leave the lawless wild-west of development, all updates, no matter how minor, must be automated and placed within deployment scripts. Software versioning must also be considered and automated steps put in place to ensure we can upgrade from any given version to any other by automatically applying one or more scripts to bring the environments up to the correct level in a disciplined, controlled and repeatable manner.

The key terms in that last statement are worth repeating: "in a disciplined, controlled and repeatable manner". That is what we are trying to achieve and this is the only way to ensure consistency between environments.

It should be easy to take a completely clean real or virtual machine and setup the full deployment environment from scratch. This should be achievable by running a single top-level script and passing in the required software version level as a command line parameter. It should also be possible to query any deployment environment and determine its current level and configuration. It would also be extremely helpful to have a script which could be run to highlight any differences between the expected environment and the actual environment. Such a verification script should be run as the first step in deployment, with the expectation it should find no deviations from the anticipated setup. But if differences are found, at least you know about them upfront and can take remedial action.

It is totally impossible to maintain any level of consistency without the use of automated scripts. Such scripts do take a bit of initial development effort to create and maintain. However the flip-side is they make the ongoing deployment tasks both trivial and repeatable.

Without automated deployment scripts, manual intervention is required. Once someone is permitted to make any manual change to any deployment environment (no matter how trivial), your environments are compromised and you are entering a world of ongoing pain.

Within large organisations, most environments will already be heavily compromised, as they will have been manually corrupted for years. Taking a lawless environment and imposing order and control over it is a long and up-hill battle. However failing to take control of your environments will ultimately waste huge amounts of time and vast sums of money.

Even within more modern container environments, involving something like Docker and Kubernetes perhaps, effort must be made to understand software dependencies and minimise image sizes; if for no other reason than limiting security attack surfaces. Also, if you happen to be pulling in numerous software dependencies, did you verify both the provence and licensing compliance for each such dependency? Allow us to simplify this question for you by supplying the answer; No - you did not. You really did not. So how do you know if you are pulling in verified libraries or images and how do you know your company is compliant in terms or licensing? Again, taking control of your environments requires effort.