Part 2 ~ Top 10 Low-Level Design Questions
These are my personal notes after reading about common Low-Level Design concepts. I rewrote them in a simpler way so they're easier to understand instead of just memorizing interview answers.
11. Why is Loose Coupling Better Than Tight Coupling?
One of the main goals in Low-Level Design is making components independent from each other.
When two classes are tightly coupled, changing one class usually forces you to modify the other. As the project grows, this quickly becomes difficult to maintain.
On the other hand, loose coupling allows components to communicate through abstractions like interfaces. Each component only knows what another component can do, not how it does it.
Benefits of loose coupling:
- Easier to replace implementations
- Better unit testing
- More maintainable code
- Easier to extend new features
Example
Imagine a payment service.
Instead of creating a PayPalPayment directly, the service depends on a PaymentProvider interface.
Later, adding Stripe or Midtrans doesn't require changing the business logic.
---
config:
look: handDrawn
---
classDiagram
class PaymentService
class PaymentProvider{
<>
+pay()
}
class Paypal
class Stripe
PaymentService --> PaymentProvider
PaymentProvider <|.. Paypal
PaymentProvider <|.. Stripe
This is one of the core ideas behind SOLID principles, especially the Dependency Inversion Principle.
12. What Are Design Patterns?
Design Patterns are proven solutions to problems that appear repeatedly in software development.
They aren't ready-to-use code, but rather templates or best practices for solving common design challenges.
Using design patterns helps developers:
- Write cleaner code
- Improve consistency across a project
- Reduce duplicated logic
- Make applications easier to extend
Think of them as architectural blueprints.
You don't copy the blueprint exactly, but you adapt it based on your project's needs.
--- config: look: handDrawn --- flowchart TD A[Design Patterns] A --> B[Creational] A --> C[Structural] A --> D[Behavioral] B --> Factory B --> Singleton C --> Adapter C --> Decorator D --> Strategy D --> Observer
Some of the most popular patterns you'll encounter are Factory, Singleton, Strategy, Observer, and Builder.
13. When Should You Use the Singleton Pattern?
Sometimes an application only needs one instance of a class.
Instead of creating multiple objects, the Singleton Pattern ensures that only a single instance exists and everyone shares it.
Common use cases include:
- Application configuration
- Logging service
- Database connection manager
- Cache manager
Imagine if every part of your application created its own logger or configuration object. Besides wasting memory, different instances could hold inconsistent states.
--- config: look: handDrawn --- flowchart LR ClientA --> Singleton ClientB --> Singleton ClientC --> Singleton Singleton --> Instance[(Single Object)]
Although Singleton is useful, avoid overusing it.
Having too many global objects can make applications harder to test because different modules silently depend on shared state.
14. How Does the Observer Pattern Work?
The Observer Pattern allows one object to notify multiple objects whenever something changes.
Instead of checking continuously for updates, observers automatically receive notifications from the subject.
This creates a one-to-many relationship while keeping components loosely coupled.
Real-world examples include:
- Stock price updates
- Chat applications
- Email subscriptions
- GUI event listeners
- Notification systems
Imagine a stock trading application.
When the stock price changes, every trader dashboard should update immediately.
Instead of each dashboard polling the server every second, the stock object simply broadcasts an update.
--- config: look: handDrawn --- sequenceDiagram participant Stock participant Trader A participant Trader B participant Dashboard Stock->>Trader A: Price Updated Stock->>Trader B: Price Updated Stock->>Dashboard: Price Updated
Observer is commonly used in event-driven systems where many components react to the same event.
15. Why Use the Factory Pattern?
Creating objects directly with new works fine for small applications.
However, larger systems often need different object implementations depending on runtime conditions.
The Factory Pattern centralizes object creation so the client doesn't need to know which concrete class should be instantiated.
Benefits include:
- Less duplicated creation logic
- Easier to add new implementations
- Better separation of responsibilities
- Supports loose coupling
Example
Suppose an e-commerce application supports several payment methods.
Instead of writing:
new PaypalPayment();
new CreditCardPayment();
new BankTransferPayment();
The application simply asks the factory:
Payment payment = PaymentFactory.create(type);
The factory decides which object should be returned.
---
config:
look: handDrawn
---
classDiagram
class Client
class PaymentFactory
class Payment{
<>
+pay()
}
class Paypal
class Card
class Wallet
Client --> PaymentFactory
PaymentFactory --> Payment
Payment <|.. Paypal
Payment <|.. Card
Payment <|.. Wallet
The Factory Pattern becomes especially useful when object creation is complex or depends on configuration, user input, or environment variables.
Instead of spreading creation logic throughout the application, everything stays in one place.
16. Why Should You Use the Strategy Pattern?
Sometimes an application needs multiple ways to perform the same task.
Instead of putting every algorithm inside one class with lots of if-else or switch statements, the Strategy Pattern separates each algorithm into its own class.
This makes the code easier to maintain and allows behavior to change at runtime.
Benefits include:
- Removes long conditional statements
- Makes algorithms interchangeable
- Easier to add new behaviors
- Follows the Open/Closed Principle
Example
Imagine an online payment system.
A customer may choose:
- Credit Card
- PayPal
- Bank Transfer
- E-Wallet
Each payment method has its own implementation, but the checkout process always uses the same interface.
---
config:
look: handDrawn
---
classDiagram
class CheckoutService
class PaymentStrategy{
<>
+pay()
}
class CreditCard
class Paypal
class BankTransfer
CheckoutService --> PaymentStrategy
PaymentStrategy <|.. CreditCard
PaymentStrategy <|.. Paypal
PaymentStrategy <|.. BankTransfer
Instead of changing the checkout logic every time a new payment method is introduced, you simply create another strategy.
17. Why Are Interfaces Important in Low-Level Design?
Interfaces define what an object can do, without specifying how it does it.
They allow different implementations to share the same contract, making applications much more flexible.
Instead of depending on concrete classes, your code depends on abstractions.
Advantages include:
- Easier to swap implementations
- Better unit testing with mocks
- Supports Dependency Injection
- Reduces coupling between modules
Example
Suppose your application sends notifications.
Rather than calling an email service directly, your application communicates through a Notification interface.
Different implementations can then be plugged in whenever needed.
| Interface | Possible Implementations |
|---|---|
Notification | |
Notification | SMS |
Notification | Push Notification |
Notification |
This approach allows new notification channels to be added without modifying existing business logic.
18. How Do You Choose the Right Sorting Algorithm?
There isn't a single sorting algorithm that's best for every situation.
The right choice depends on your data and system requirements.
Things to consider include:
- Dataset size
- Available memory
- Data distribution
- Stability requirement
- Execution speed
- Whether data is stored in memory or on disk
For example, sorting one thousand records is very different from sorting hundreds of gigabytes of data.
Common Choices
| Situation | Recommended Algorithm | Reason |
|---|---|---|
| General-purpose sorting | QuickSort | Very fast average performance |
| Stable sorting | Merge Sort | Keeps equal elements in order |
| Nearly sorted data | Insertion Sort | Minimal overhead |
| Huge datasets on disk | External Merge Sort | Doesn't require loading everything into memory |
| Small datasets | Insertion Sort | Simple and efficient |
Choosing the right algorithm is about understanding the trade-offs instead of memorizing time complexities.
19. How Can You Evolve an API Without Breaking Existing Clients?
Software rarely stays the same forever.
As applications grow, APIs need new features, additional fields, or improved behavior. The challenge is introducing those changes without breaking applications that already depend on the old version.
This is where versioning and backward compatibility become important.
Common practices include:
- Versioning APIs (
v1,v2) - Database migrations
- Feature flags
- Gradual deprecation
- Regression testing
Example
Suppose version 1 returns:
{
"name": "Uqie"
}
Version 2 introduces a new field:
{
"name": "Uqie",
"avatar": "profile.png"
}
Instead of replacing the old endpoint immediately, both versions continue running until clients finish migrating.
--- config: look: handDrawn --- flowchart LR ClientA --> V1["/api/v1/users"] ClientB --> V2["/api/v2/users"] V1 --> Database V2 --> Database
This strategy allows new applications to use the latest API while older applications continue working normally.
20. How Does Authentication and Authorization Work in Distributed Systems?
Authentication and authorization solve two different problems.
- Authentication answers: Who are you?
- Authorization answers: What are you allowed to do?
Modern distributed systems usually separate these responsibilities using an Identity Provider and token-based authentication.
A common workflow looks like this:
- User logs in.
- Identity Provider verifies the credentials.
- A JWT or access token is generated.
- The client includes the token in every request.
- Each service validates the token before processing the request.
Popular technologies include:
- OAuth 2.0
- OpenID Connect
- JWT
- Multi-Factor Authentication (MFA)
- Role-Based Access Control (RBAC)
--- config: look: handDrawn --- sequenceDiagram actor User participant IdentityProvider participant API Gateway participant Order Service User->>IdentityProvider: Login IdentityProvider-->>User: JWT Token User->>API Gateway: Request + JWT API Gateway->>Order Service: Forward Request Order Service->>Order Service: Validate Token Order Service-->>User: Authorized Response
In a microservices architecture, every service validates the token independently before serving the request. This keeps services secure while allowing users to authenticate only once.
Final Thoughts
As you learn more about Low-Level Design, you'll notice that many concepts are connected.
For example:
- Interfaces help achieve loose coupling.
- Factory creates objects.
- Strategy changes behavior.
- Observer distributes events.
- Singleton manages shared resources.
Learning each pattern individually is useful, but understanding when and why to use them is what really matters.
A good software design isn't about using every design pattern—it’s about choosing the simplest solution that solves the problem while keeping the code maintainable and easy to extend.