Best practices & guidelines

Lukas Liesis
9 min readApr 22, 2024

Code is just a tool. Some abstraction which you can’t even touch. It’s a tool, which for some is the closest thing to superpowers, but still, it’s just a tool to create, discover and make dreams come true.

I focus on building business value with this tool. I started coding web back in 6–7th grade when most PCs had no internet and documentation was actual printed book. From 7th grade already attended 2nd school of web tech and couple years later was already building my own game servers. Fast forward couple decades to today and I still have same obsession for tech.

Here I share my insights and best practices which I found so far to work the best for me. I understand different developers may have different preferences and I’m always looking how to improve all aspects described below, comments are highly welcome. Enjoy.

Software engineering

Software engineering is a creative, complex and dynamic activity. Code requirements change constantly, developers can resolve the same issue differently on different days. When dealing with any complex, dynamic systems the world’s best engineering approach is the scientific method. It is widely used with great effectiveness in all kinds of engineering works, software engineering is no different. When it’s outside of “hello world” sample scope, software quickly becomes complex and dynamic. Change on one side may create unintended results on another. It becomes ever harder to define software in deterministic explanation.

TDD is the closest thing to the scientific method for software engineering. Unfortunately, it’s often interpreted in the wrong way because of high-pace headlines-reading activities. Most visible part of TDD is the high percentage of test coverage. Test coverage is never the goal of TDD, it’s just one of the effects. Most visible, easiest to talk about, hence usually becomes at center of discussions related to TDD, but it’s important to see how it’s not a goal but a by-product.

TDD starts from interface design. Inside the test environment the developer defines the best I/O communication with the software one builds. Abstraction from the complexities of software bit. Goal is to make the easiest interface to consume the software resource. Single test environment is used to design a single use case interface of a single unit of software. It’s like nano-service on its own, living its own life, in a perfect scenario without external factors. Usually, many software components must be integrated between each other, that’s why the I/O interface of each is so important to be designed well. Well designed interfaces allow easy build of bigger structures at higher quality and pace.

TDD encourages to slice down the system and focus on one component at the time. Design it well before moving forwards. Design the interface and then work on implementation details. One can say it’s micro services approach to the smallest contextual degree. Process of designing interfaces first forces into the mindset how to make software more decoupled from one part to another. This increases adaptability and high quality software is often defined by its ability to change over time. This ability naturally accrues in this process.

Industry “best practices” like separation of concern and modularity are enforced naturally by the design process itself. It becomes easier to build high quality software. By engineering software with these practices, code coverage just happens. Modularity just happens. High flexibility and adaptability of software just happens.

Developer spends time on interface design. Developer needs to run the code and the code is running inside the test environment. It becomes very easy to add a bit of devops effort on top to re-run these tests after all code changes and you suddenly have a full package of TDD and CI. This increases trust on while after each commit software falls into releasable state, that’s why naturally CD part is often added to actually deliver the software updates continuously to the end user, it’s possible because of the trust.

Developers have way higher trust factors on engineered versus coded software.

Major point of TDD is interface design. If the design is bad, the test will become complex, it will be hard to use the interface and the developer will want to find a better solution from the get-go. It emphasizes proactive problem-solving and avoiding the need for fixes or workarounds later on. This is quality software. This is an environment where developers are proud of their work.

Such a feeling causes many other positive effects outside of the software engineering itself. When people talk about the workplace, the topic starts to focus on values rather than implementation details, this is way better marketing message to gather better leads for hiring process and keep retention metrics above average.

Micro services

Micro services are hard because it’s multi-thread environment with all the multi-threading issues like race conditions.

Micro services without API contracts are acting like distributed monoliths. Developers lose on both fronts, you don’t get benefits of monolith while it’s distributed and you don’t get benefits from micro-services, while those are highly coupled.

When saas deals with money, database layer transactional writes are needed in some cases, those are very hard to achieve in a multi-threaded environment.

Micro services should not be built around objects in the database, but rather bids of business logic, like micro saas which live on their own, don’t have UI, but expose API so other devs could use these mini saas. This unlocks all the benefits of micro services — teams can work w/o worry of destroying something else. Onboarding is simple, the requirement of “knowing it all” to make changes drops.

Micro services can slice down business logic to tiny individual micro saas and it becomes much easier to argue about its state in comparison to full saas at once.

These little things help a lot to be more efficient, consistent, onboard new people and overall build software at higher pace and quality. It’s easier to attract new talents while a well designed system is worth sharing on stage and content allows to build a dev community around, hire them.

Well designed system increases satisfaction of the work per person in the team. This helps to reduce stress, increase productivity, makes positive social impact inside the company, helps to build exciting company culture which attracts people. While these things have direct influence on many other processes — QA, PM, updates, a lot of people feel such effects.

Code format

For formatting use prettier with default configuration. If in doubt — check “Why Prettier?” at https://prettier.io/docs/en/why-prettier Eslint is optional and it must be used only for code-quality needs while all format-related eslint rules must be disabled. For any discussions related to code format, use prettier community forums and follow their guidelines on how to introduce changes.

Naming conventions

Naming is said to be one the hardest things to get right while building high quality software. Good naming allows faster onboard on new code, understanding it better without need of documentation or comments.

Variable & Function Names: camelCase for names (e.g., firstName, calculateArea).

Class Names & JSX functional components: PascalCase for class names (e.g., Customer, ProductManager).

Interface Names: Use PascalCase for interface names (e.g., UserInterface, SettingsOptions).

Constants: UPPER_SNAKE_CASE for constants (e.g., MAX_VALUE, PI).

Private Members: Prefix private members with an underscore (_)

Boolean must start with the form of “to be” e.g. isGoing, isEnabled, areGoing, wasGoing. Preferred prefix is “is”.

Time

Unix timestamps in seconds must end with “At”, in milliseconds “AtMs”. E.g. `createdAt`, `createdAtMs`.

Math

Counting any math should be made with integers and rounded up to required precision. If float-number math is needed, it should stay inside helper functions only and not exposed directly through the API I/O, because, like in many programming languages, in TS `0.1+0.2 === 0.3` is false.

Money values

Money values should be saved in minimum required accuracy, usually it’s cents but it could be less too. If it’s cents, the variable should end “Cents” e.g. `totalPostDiscountCents` and it would be integer to avoid floating point math issues as `0.1 + 0.2 !== 0.3`.

IDs

When creating new objects, usually the object needs an id.

First part of IDs should be a prefix identifying the object, e.g. user id `u-`, invoice id `in-` and so on, it should be 1–3 letters long prefix identifying the object. If prefixes are kept unique throughout the system, it will be very easy to debug with only the id, while ID itself identifies the origin of the object.

Second part of ID should be unix timestamp in ms when ID was created, this provides 3 key benefits:

  1. Index on id field automatically provides order by creation time, which is often the default way to display lists.
  2. From only the id itself can tell when object was created
  3. Effective pagination when ordered by creation time. With databases usually lag comes when you need aggregation, pagination is one of most popular places where aggregation happens. It’s often a silent issue which becomes visible once there is enough traffic on saas. With such IDs can paginate effectively without any other fields by using $gt operator on mongo, or equivalents on other databases.

Third part of ID should be random characters by avoiding visually similar characters (eg: 0Ool) to reduce debug time for miss-spelled IDs, easier share IDs between user and support, QA and devs etc.

Object ID is often a data point available in logs, errors etc. It’s providing real benefits when done this way, easier to debug and report issues, resolve them. Need less context in logs, reduces amount of exposed data, reduces size of logs, which increases speed searching the logs too.

Sample IDs:
Flow: fl-17135571301071wKLazaGISxM
API Key ID: apid-1713556988853m9VRjcdlptLt
Billing plan: bp-1713770928215M3AVj7QF2Hti

Logging

Logs are essential for debugging. Log which just prints some text is harder to find in comparison to a log which prints its ID as a prefix.

The ID of the log line allows instant find where the log happened. E.g. if log message from user provided screenshot from console says “Not found” vs log message “#2024102145341493 Not found”, for first one developer will waste find debating if it’s coming from UI or backend, once decided which part has more chances to show such log, will have to go through all kind of places which seems to be related to the view from screenshot, but if screenshot has only error log, then context is completely lost and it’s close to impossible to tell what happen.

Meanwhile, a second log message can search all code bases and see the exact log place within seconds with or without full context provided by user or QA.

With the same ID you can easily search the history of logs and see how often it happened before, in git you can exactly see when it was introduced. There are many benefits from adding a unique ID to each log message and it’s simple to do too.

The simplest way to add ID is by just typing random ±12 chars while logging `console.log(“#343roudfhsjd Not found”)` it may sound counter intuitive but the probability of pressing exactly the same combo is very low.

A little bit more advanced but way more useful is to use your IDE’s live templates feature which allows a predefined template and use it as a code snippet. I personally have `cl + tab` which writes something like `console.log(`#2024417131942978 `, )` The first part is the current time which also allows us to see the date when log happens (YYYYMDHMSs). Cursor jumps to type text after the id and tab lands to the final place after the comma. This approach allows you to quickly log with a perfect ID every time.

This speeds up debugging tremendously, while from any log line can copy the id and jump to the exact location where the log happens.

If using any log aggregation tool for logs managing, this allows to search how often this exact log happen over time.

Language selection

For the last decade there are billions invested to make JS & TS developer-friendly, communities were formed, a vast range of all kinds of tooling was and is invented. This language still continues to get a lot of investment from a lot of big players in the market. You can read here about “Atwood’s Law” from 2009, which stated Any application that can be written in JavaScript, will eventually be written in JavaScript https://qr.ae/ps411w

This trend is going strong till date and there are no real signs for it to change.

JS services are moving to TS. TS allows to have types. Types allow to get better developer experience with better autocompletio and type-safety for all architectures while building the code. It helps a lot with testing, mocking, onboarding, integrations, code share.

Another language introduction increases friction, complexity, chance for bugs, slows down debugging and updates, and introduces everything that high-quality software would like to avoid. If there is any edge case scenario where another language is a must and no developer on the team has anything to offer how JS/TS can not be used for it, then there could be a tiny layer to expose missing API to JS interface. Usually it’s a CLI-based executable as a sub-process which would be executed for the case.

Every technology requires know-how, edges of it, debugging experience and splitting attention to multiple languages is destroying dev productivity and deep understanding of the tools on hands.

--

--

Lukas Liesis

2 decades exp of building business value through web tech. Has master's degree in Physics. Built startups with ex-Googlers. Was world’s top 3% developer.