Hello, fellow lead/architect/experienced developer, and congratulations on getting here and finally having the opportunity to not only design a technical solution of a specific problem or a class hierarchy but a full-scale architecture of a whole set of applications from the grounds up! Let’s dive deeper into the best practices around software architecture.
So, you’ve come to the point where you have the freedom to choose what everything runs on, what frameworks and tools to use etc., and you’re probably wondering what the good practices are. Well, in this article, we will be looking at one key aspect of creating an architectural solution – making your software future-oriented.
In my humble opinion, there are two very important aspects you need to pay attention to here – keeping things scalable and making your code easier to configure, extend and change.
Starting from scalability – we have a few different aspects here. They can be broken down into:
1. Scalability of Performance
That’s an important one! Performance is actually a key to happy customers, and we must ensure that our application will perform well under the expected load and conditions. Here are a few tips on how to actually achieve this:
Bottlenecks in performance often happen when multiple “consumers” (e.g. processes, services, applications) rely on accessing the same resources – be it database, another service or maybe system resources. To avoid these, you can try:
- Caching – often used resources should be cached when applicable – for example, you keep your configurations in the database – don’t make a call every time, and don’t rely on your ORM to cache the query – cache the method call yourself! EHCache is awesome and easily configurable with Spring – you simply need to set it up, and then it’s a matter of using a simple @Cacheable over your method.
- Non-blocking I/O – make sure to use read/write calls on your resources, services etc. which are non-blocking as much as possible – don’t wait for slower operations to be done first. Instead, try to parallelise things as much as possible. Think of the way Controllers (well, technically – Servlets) work in Spring Boot – you don’t wait on one request to be finished first, your API is serving multiple requests in different threads. Design every call with this in mind, if possible.
- Load-balancing – we touch on this in the next section, but if there is a chance something needs to be run in a blocking way – maybe you can have multiple instances of it, e.g. your database server. Too many requests? Make the servers two, and put a load balancer in front of them. Kubernetes allows for load balancing in just a few easy steps!
Scaling your Instances
With great consumption must come great providing! Your user count will (hopefully) grow, so your applications must adapt to be able to handle this additional load. Here are a few tips on how to achieve this:
- Always, always, ALWAYS design your whole infrastructure with horizontal scaling in mind. Avoid relying on vertical scaling, as it is often not even possible and definitely not a sustainable way to achieve scalability – you only have so many processor cores and RAM you can add. Horizontal scaling, on the other hand, helps with balancing the load on more nodes, and those nodes can be created/started on demand – absolutely lovely feature of tools like Kubernetes, which allow for the creation of new pods on demand, depending on the needs of the application.
- Try to avoid depending on physical servers – the Cloud is here, and it has changed the way we even think about performance, deployment and infrastructure. Storage on the cloud, for example, is spread across multiple systems and is distributed, and can also very much be scaled upon your needs – no point in paying for 50TB of storage when your users only generate 50GB of content. Also, there is no need of a huge initial investment even if you are certain you’ll reach big demand in storage in a while – businesses love low starting costs. Check out AWS and S3 for awesome storage options!
- Microservices – that practice basically just puts almost all we’ve just said into an actual way to build applications. It spreads the load even more, making tweaks to where you need extra performance even easier – if you have multiple services, each responsible for a very specific thing, and you see that thing is taking its toll on your system’s performance capabilities, you’re able to increase the number of nodes for just that microservice.
2. Scalability of Maintenance
So, we have created a great application architecture, but now we have to also maintain our (multiple, if we followed the microservices pattern) applications. Software has to be written in a way that makes maintenance, changes and additions easy – here are a few tips on how to achieve that:
- Make sure you have an adequate way to monitor your application – this ensures that what you planned to do was actually done. Monitoring your live application can be achieved through tools like Actuator for checking your app’s health status and the ELK stack (Elastic, Logstash, Kibana) to make sure you have logs that are always available and easy to search through. I would also add that monitoring your software includes not just what happens during uptime – make sure to monitor other stats too, for example with static code analysers like Veracode or SonarQube, so you can at least have a general idea of how reliable your software is. Also last, but not least – try to monitor your development cycle as well – my experience says that by improving your process, you improve the results as well, so always try to check what went wrong (as, obviously, things DO go wrong sometimes) and learn from your mistakes in order to improve your future cycles.
- All these tools, however, can’t really tell you how easy it is to read or extend your existing software. Here, in play come the best practices while actually designing your project’s structure. I, personally, am a big fan of the Hexagonal architecture when designing software – it’s derived from the concept of Clean architecture, with the only difference that you have an extra layer of abstraction and separation between the Domain layer with all your business logic and domain entities, and your outside world, in the shape of ports and adapters, which are easily configurable to be swapped, for example using an I/O port for your storage needs and an adapter to a database, then plugging another implementation through the adapter setup, maybe a simple csv file, and configuring all of that in a Spring bean configuration. Easy, right?
Summing it all up, the best way to ensure your software systems are ready to be utilised and built upon for many years to come, is to make sure both the infrastructure and the code are ready to be changed upon need. Good luck out there!