This is something I strongly believe inand I also encourage other programmers to follow when I am in a leadership role
The work that you will be doing will echo your personality, believes, discipline, ideas and level of complexity long after it has been written. It is there for crucial to follow standards and practices that aligns with the company policy as the code you write is defining the company itself. This is a guide I wrote to ensure a consistent code base and principals.
If it is not documented, it does not exist! No exceptions.
- Document on the Readme file of the GIT repository, example if it is an API. Then document requests, responses, authentications, throttling and provide small descriptions of functions.
- Then also document whole modules of the system, example; how does it work, what DB tables does it use, what support need so be done to maintain or setup the module, how to do trouble shooting, etc.
- Always keep documentation up to date. It will help your fellow team mates if they also work on the project and fight technical debt.
Security starts with us, the developers. No one is going to force you to add security right from the start, it has to be a choice. Sure it might be caught in code reviews, but no one is going to be your co-driver to help you make the right decisions. In fact, if coding securely the first time is not already an absolute in your mindset, start adopting it now. Even if it means you will miss your deadline, do not rush security and test it thoroughly. Things to keep in mind:
- API’s must have Security Credentials, either in the headers (Basic Auth is fine) or a custom method of validation inside the request. HTTTPS is a must.
- Storage of API keys, where are the stored, who can access them, how quickly can I replace the API key if it gets comprised.
- Passwords MUST be hashed the DB, not encrypted or left in plain text format.
- Personal identifiable information must be encrypted, things like phone numbers, names, email addresses, ID numbers.
- Authentication is the process of verifying who you are. Example: when you log on to a PC with a user name and password you are authenticating. Authenticate users on every request. For PHP use the $_SESSION object (uses file system), for a standalone node.js API, use the DB or a cache like Redis as the storage level. After a user logged in, store in a record a newly generated session id, let the user then on all subsequent requests attach this to the body of the request.
- Authorization is the process of verifying that you have access to something. This should happen after Authentication. It Verifies if you have permissions to access a resource (e.g. directory on a hard disk, admin web page).
- Data Validation. Do not trust any user input ALWAYS do server side validation. It might be considered okay if you have an API, to not validate user input at every step IF the API is for internal use only. If the API is exposed to third parties, always add validation.
- Data security. Example: when deleting a record make sure it belongs to that client, otherwise a smart user, can just type in a random id to delete some data that does not belong to him. Then make sure all S3 documents are private first then make them public when needed. Least privileged mode first, then relax permissions afterwards. Also delete S3 records when deleting associated DB records
- Data validity. Do not create the phone number field as a varchar and then on the front end allow any type of text and numbers. Clean data is essential for the DB and applications to work properly. On the front end, a check needs to be done to ensure all phone numbers are in international format, prefixed with a +, no spaces and only numbers. This will provide a data consistency that can be “trusted” by all applications (current, future, third parties). Garbage in Garbage out.
Build for maximum reliability, think of all possible outcomes, not only successful ones. You should have full confidence when going to bed at night that you will not be woken at 2 AM with a frenzy of system generated error emails. If your code fails, it should recover by itself or if this is not possible at all then fail fast so that another action to remedy the situation takes place.
- Self-recovery. An example of self-recovery(retry logic) is to send requests to queues (DB storage) .If you rely on a third party, for example to send a SMS/Email/Push always have a solution ready for when their APIs you are calling is down. This means Log (save in DB) all data before it is send, then try to send it, if it fails increment a counter on that record that failed with reason and then the time of when the request failed. Then have a periodic checker, maybe once a minute, which does some logic to retry sending that request. Here an exponential back off strategy works great.
This way if there is a failure downstream, your system will recover because it has now build up a queue. Just be careful if using synchronous queues, as it will mean that Client A will have to wait for Client B’s request to finish. To remedy the synchronous problem, do a query to split up requests per client and then send each of their request synchronously. Better yet always go for async queues if the third party provider supports them.
- Fail fast. An example of failing fast would be on the front end web interface (portal). If an error occurred it would usually be a runtime error, for example: a user does not have the correct permission so then he tries to access an object from session that does not exist. Fail fast otherwise the user will see an error message on the screen with a partially data loaded screen, this data might be sensitive as he does not have the right permissions to view them for reason.
Error handling determines how well your reliability procedures will work. All errors need to be catched and acted upon, plan for the worse. This action can just be simple logging or notifying a responsible person to fix the error. Here are some things to keep in mind:
- Log everything. When an error happens, take a snapshot of the error, for example at least save the time and some identifying information so that you can go look it up.
- Always keep to a pattern when writing logs, this will enable you to then search for keywords to narrow your search. Do not just write strings as you like. Example pattern: [Serverity Level] – [Date and Time] – [One liner error message] – [JSON object with more info]
- All user interactions should be logged (this is easy with cloudwatch and API gateway lambdas). Also on the front end, it is not needed to log each page load/request. A good strategy is to save the last 5 page loads/requests to session and then when an error occurs, send all that “high level” stack trace with the error log, also include the session. This is very useful as you then have the user steps to reproduce the error.
- Reliability. Either implement a self-recovery(Transnational, Saga pattern) or a fail fast system, irrespective of choice, log everything both of them do.
After code is written it needs to be maintained. Not only your code but also the framework it relies on, this means if your code depends on frame work A and framework A has an update, then it is your responsibility to also keep the framework up to date. Otherwise if you keep pushing this off, you will find yourself (or your team mates/the next developer) in a position where the framework is 10years old and development on the framework has stopped on it in the 5th year. To convert from a dead or very outdated framework to a new one is a major task, where it all could have been avoided if you and your team, for example, just takes 1 week in each year and then upgrade all the frameworks to the latest versions.
Technical debt also gets added when nothing is documented and a single person is responsible for that code/feature of the system. It is the team’s responsibility to ensure that this does not happen, they must push back at the deadlines and management to fight technical debt.
- Dumb + Simple (KISS)
- Writing complex code might be rewarding on a personal level, but it is not encouraged in team development, unless the whole team is on that same level. If they are not, for other team mates it will be difficult to maintain and continue with your work. So always keep the next developer in mind.
- Rather write more code if it is simpler to read and understand, than writing that 1 liner regex expression that takes 2 years to understand properly.
- Avoid getters and setters at all cost, it hides complexity. Using methods and functions you can achieve the same functionality.
- Keeping in mind to find the best solution not a solution, it must be extendable in the future. Do not implement it yet, just leave room that if it needs to be extended then it will be possible without completely redesigning the system.
- Readable + Clean
- Naming variables and functions should be self-explanatory
- Name positively, example instead of naming a variable: isDisabled, rather name is isEnabled. Our brains process positive language much better than negative language, example: for isDisabled you will have to think in the line of: if something isDisabled then it means it is not enabled so this piece of code will not work if isDisabled is true. Where it could have been much easier to make the association if it was named as isEnabled.
- Break long functions up in sub functions.
- Only write functions for code that gets reused. If you copied and pasted something, you should most probably have written a function for it.
- If you write code for example in a method that has similar functionality and that code is just used in that method, then it is not needed to break it into smaller functions as it will only be used in one place. Unless that method grows to for example 300+ lines, then it can be broken up in to smaller functions. Not for the sake of reusability for the smaller functions, but rather to increase readability of the bigger function it is used in.
- Avoid nested code, rather exit early from the function.
- Consistency is what makes large projects manageable. The whole team needs to try and stick to the consistency of the project. It must look as if one developer on his own coded everything, you should not be able to, by just glancing at a piece of code and state that developer A wrote that.
- Strive for consistency, even if it is wrong. For example, if you notice that all tables in the DB are in snake case (small letters separated by underscore), you should also keep to the same naming as current developers already assume everyone to follow the same strategy.
- Only when needed. KISS, do not try and break a simple problem into 1000 classes and interfaces(Class explosion), this greatly increases complexity, maintenance and decreases readability.
- Consider other approaches as well, functional programming. OOP is not always the only option.
I am not going to explain SOLID hear in detail, please google it and find a good article. Below is the essential concepts to take a way (my perspective)
- SOLID principles
- Single Responsibility Principle – A class should only have one functionality/job. Example, a person class should only do person functions, you should not have anything that describes that person’s pets and cars in that class, only person related functionality like age and eye color. You can then create other classes for pet and vehicle.
- Open Closed Principle – Objects should be open for extension and closed for modification. This essential means that every time requirements changes you should not go back and change the core of the class. Rather extend it, by just adding to it. Example will be to use the person class as base class for an Asian person and the African person class. This way if you change the base person class, you will change both African and Asian classes as well, as they extend the base person class.
- Liskov substitution Principle – This is usually when working with OOP, specifically interfaces. If a class inherits from an interface or base class, then it can be casted to that base class or interface, thus it does not matter if the person is Asian or African, we are certain at any time, they both have basic functions of a person. This ties in with Dependency injection, these two points + the factory design pattern is often used together.
- Interface segregation Principle – If a client/user of the code only wants a certain functionality then do not give more than that. Example, if they are only interested in the real time features of a person, providing them with the history of the whole family is unnecessary. This is done by applying only the needed interfaces to a class and then creating objects with only certain interfaces, not all of them. This point is not as important as the others.
- Dependency Injection Principle – This is basically the same as point 3. If you did all of the other points correct then you will now be able to code against interfaces, not classes. This means if you have a logging interface, and 2 classes that inherit from it, a file logger and api logger. Both should have a log function, thus you can pass either a file logger or api logger to your main error logging function that expects a logging interface. So then your function arguments takes in interfaces and not concrete classes.
- Design patterns
- Do not force the application of them. Many times you can get away by just using normal OOP or functional programming.
- Keep it simple if you do decide to implement it, design patterns introduce complexity, if not understood correctly.
- Try and keep to the basic most used patterns and if you do not know the below, please read up on it:
- Singleton (great for your main DB class)
- Factory pattern (great for code that has similar structure but the internals change for each implementation)
- Once again do not over abstract. Even though you can OOP or apply SOLID, you do not have to, as it introduces more complexity and reduces readability.
- Comments should not be too verbose, write only what is needed to get your point across.
- When writing comments keep to the essentials only. As code changes and many times comments just get overlooked, then the code and comments does not agree. So only write essential comments that will “stand the test of time”.
- Single line comments explain what a single line of code does.
- Multi line comments are used explaining a function or how the process/concept of many lines of code work.
Minimal Viable Product. Only do what is in the requirements, do not start with future functionality. Otherwise you will do unnecessary work and waste time to create something that hardly gets used. As the system gets used, you will probably get feedback and that will determine the change requests to the next version of that system. Thus always do the basic requirements, and as usage increases features can be added and the system made more robust.
Be “lazy” in the sense of working smart with your time.
The dev team is a collective, a shared ownership. There is no I in team, if your team mates write bad code the whole team will be blamed as a whole. Responsibility falls on the team not person. Thus to achieve this unity, code reviews must be used. Before merging the new code at least 2 developers must also read, understand and ensure that all principals are used and followed correctly.
Enhance an implementation. Do not criticize it. Remember developers are also persons, consider their current situation. Give reviews the same way you would like to receive them.
Keep in mind your personal life. Junior developers are eager and motivated which is great but they often run the sprint early on and that is fine for a month or so, after that you need to switch to marathon mode to go for the long run. Below are some things to keep in mind.
- Stick to daily routines, it increase productivity.
- Do not forget to exercise.
- Keep healthy eating and sleeping habits
- Do not neglect relationships
- When frustrated, get up and get a glass of water, just take a break. Speaking to yourself (become the duck) or another developer always helps. You are not alone and other devs probably goes through the same problems.
- Never use the word JUST, if you do, multiply your deadline by 2.
- Some people are makers, other are menders. Decide what you are and if you are not where you want to be, you still have time to work on it and get there.
- Learn how to say no and negotiate. Be humble
- Admit/own your mistakes, we all make them