What makes code maintainable?

Maintainable code is code that is easy to understand, modify, test and extend. It should have clear naming, simple structure, small functions and consistent formatting. Good maintainable code follows SOLID principles, avoids unnecessary duplication, and separates responsibilities clearly.

It should also include meaningful comments where needed, but the code itself should be readable without too many explanations. Automated tests, good error handling, and clear documentation also make code easier to maintain over time.

The common practices for writing maintainable code include:

  1. Create an uniform coding style and follow it consistently.
  2. Use meaningful names for variables, functions, and classes.
  3. Keep functions and classes small and focused on a single responsibility.
  4. Avoid code duplication by reusing code and creating utility functions.

SOLID:

  • Single Responsibility Principle: A class should have only one reason to change.
  • Open/Closed Principle: Software entities should be open for extension but closed for modification.
  • Liskov Substitution Principle: Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
  • Interface Segregation Principle: Clients should not be forced to depend on interfaces they do not use.
  • Dependency Inversion Principle: High-level modules should not depend on low-level modules. Both should depend on abstractions.

What is denormalisation in data schema design? What are some challenges with denormalisation?

Denormalization is a database design approach where duplicated or redundant data is intentionally added to tables to improve query performance. Instead of splitting data into many related tables, some related information is stored together to reduce joins and make reads faster.

The main challenge is data consistency. Because the same data may exist in multiple places, updates must be carefully managed to avoid inconsistent records. It can also increase storage usage, make write operations more complex, and require extra logic to keep duplicated data synchronized.

What makes a good abstraction?

A good abstraction hides unnecessary implementation details and exposes only what the user of that abstraction needs to know. It should make the system easier to understand and use, not more complicated.

A good abstraction should have a clear purpose, a simple interface, and should be flexible enough to support future changes without exposing internal complexity. It should also avoid being too generic too early, because over-abstraction can make code harder to read and maintain.

What is a “race condition”?

A race condition happens when two or more processes or threads access shared data at the same time, and the final result depends on the timing or order of execution.

For example, if two threads update the same counter at the same time, one update may overwrite the other and produce an incorrect result. Race conditions can be prevented by using proper synchronization techniques such as locks, atomic operations, transactions, or avoiding shared mutable state.

What are the key challenges in designing distributed systems?

The key challenges in designing distributed systems include network latency, unreliable communication, data consistency, scalability, fault tolerance, and system monitoring.

Because different services may run on different machines, failures are expected and the system must be designed to recover gracefully. It is also important to handle retries, timeouts, duplicate messages, and partial failures. Another major challenge is balancing consistency and availability, especially when data is replicated across multiple services or databases.

CAP Theorem: In a distributed system, you can only guarantee two out of the following three properties at the same time:

  • Consistency: All nodes see the same data at the same time.
  • Availability: Every request receives a response, without guarantee that it contains the most recent data.
  • Partition Tolerance: The system continues to operate despite arbitrary message loss or failure of part of the system.

Pick a database engine e.g. Postgres. Explain the behavior when two processes concurrently increment the same data.

In PostgreSQL, if two processes concurrently increment the same value using an atomic statement like:

1
UPDATE accounts SET balance = balance + 1 WHERE id = 1;

Postgres will handle this safely by using row-level locking. The first transaction that updates the row will lock it. The second transaction must wait until the first transaction finishes, then it will read the latest committed value and apply its own increment. So both increments are preserved.

For example, if the original value is 10, after two concurrent increments the final value should be 12.

However, if the application first reads the value, calculates the new value in application code, and then writes it back, a lost update can happen. For example, both processes read 10, both calculate 11, and both write 11. In that case, one increment is lost.

To avoid this, we should use atomic update statements, transactions, row locks such as SELECT … FOR UPDATE, or appropriate isolation levels depending on the use case.