Java and LLD
Java and LLD
Low-Level Design
● High-Level System Design focuses on the broader architecture and structure of a software
system.
● It deals with defining the system’s components, their interactions, and the overall flow of
data and control.
● High-Level Design aims to provide a conceptual framework that outlines the system’s
functionality without delving into the finer details.
● Abstraction: Focuses on the system’s overall structure and functionality without getting
into the specifics.
● Modularity: Emphasizes breaking down the system into manageable and reusable
components.
● Scalability: Ensures the design can accommodate growth and handle increased loads over
time.
● Flexibility: Allows for easier modifications and adaptations to changing requirements.
● Overall System Architecture: Defines the system’s high-level structure, identifying major
components and their communication protocols.
● Functional Requirements: Specifies the system’s intended functionalities from a user
perspective, focusing on “what” the system will do.
● Non-Functional Requirements: Outlines performance needs, security considerations,
scalability requirements, and other non-user-facing aspects.
● Low-Level Design, on the other hand, focuses on the detailed parts of the software system.
● It focuses on defining the internal logic, algorithms, data structures, and interfaces of
individual components
● identified during the High-Level Design phase.
● Low-Level Design translates the high-level architecture into a detailed blueprint that
developers can use for implementation.
● Component-Level Design: Breaks down HLD components into smaller modules, defining
their functionalities, interfaces, and interactions.
● Data Structures and Algorithms: Specifies the data structures and algorithms used within
each module to achieve the desired functionalities.
● Database Design: Defines the structure of the database, tables, and relationships for data
persistence (if applicable).
● Early Stages: Ideal for defining the initial architecture, scope, and requirements of the
software project.
● Agile Environments: Well-suited for projects that adopt an agile methodology, allowing for
flexibility and adaptability.
● Complex Systems: Useful for large-scale projects with multiple components and
interactions where a bird’s-eye view is essential.
● Advanced Stages: Necessary when transitioning from the design phase to the
implementation phase.
● Specialized Components: Required for designing specific algorithms, data structures, or
interfaces in detail.
● Performance-Critical Systems: Essential for optimizing performance, resource utilization,
and overall system efficiency.
Planning a Wedding:
● HLD: Sets the overall theme, budget, guest list size, venue selection, and desired flow of the
event (ceremony, reception, entertainment).
● LLD: Details the menu for the reception, decorations for the venue, specific songs for the
playlist, and logistics like transportation for guests or vendor schedules.
High-Level System Design provides a strategic overview and sets the direction for the project, Low-Level
Design offers a detailed roadmap for implementation and optimization.
● Units of code should be open for extension but closed for modification.
● Extend functionality by adding new code, not modifying existing code.
● Useful in component-based systems like a React frontend.
Example: Consider a payment processing system that handles payments via credit cards. If you
need to add support for PayPal, rather than modifying the existing code, you should extend it by
adding a new class for PayPal payments. This ensures the existing system remains stable while
allowing new functionality to be added.
Example: If we have a Bird class that has a method fly(), and we create a subclass Penguin, which
cannot fly, this violates LSP. The Penguin class should not inherit fly() since it changes the expected
behavior. Instead, the Bird class should be refactored to handle birds that can and cannot fly
differently.
Example: Suppose we have an interface Animal with methods fly(), swim(), and walk(). A class Dog
that implements Animal would be forced to define fly(), which it doesn't need. To comply with ISP,
we should split the Animal interface into smaller interfaces like Flyable, Swimmable, and Walkable
to avoid forcing irrelevant methods on classes
\
Example: In an e-commerce application, if the checkout process (high-level module) depends
directly on a specific payment gateway like PayPal (low-level module), changing the payment
gateway requires modifying the checkout process. By introducing an abstraction, such as a
PaymentProcessor interface, the checkout process can work with any payment method without
needing to know the specifics of PayPal or any other service.
Example Problem Ostrich can't fly but inherits Robot forced to implement
fly() eat()
"Can a class violate SRP but still follow OCP?" Deep understanding that SOLID principles are
independent but complementary.
"How would you enforce SOLID in a Can you apply SOLID at the system level, not
microservices architecture?" just code?
"Is it possible to over-engineer by blindly Testing if you know where not to overuse
following SOLID? Give example." patterns.
"Explain how LSP and ISP are connected." Deep knowledge — both are about correct
behavior and decoupling.
"What's the tradeoff between SRP and Critical thinking, because more classes = more
performance in real-time systems?" complexity sometimes.
Microservices design patterns provide tried-and-true fundamental building blocks that can help
write code for microservices. By utilizing patterns during the development process, you save time
and ensure a higher level of accuracy versus writing code for your microservices app from scratch. In
this article, we cover a comprehensive overview of 10 microservices design patterns you need to
know, as well as when to apply them.
Key benefits of using microservices design patterns
Knowing the key benefits of microservices will help you understand the design patterns. The exact
benefits may vary based on the microservices being used and the applications they’re being used
for. However, developers and software engineers can generally expect the following advantages
when using microservices design patterns:
The database is one of the most important components of microservices architecture, but it isn’t
uncommon for developers to overlook the database per service pattern when building their
services. Database organization will affect the efficiency and complexity of the application. The
most common options that a developer can use when determining the organizational architecture
of an application are:
A database dedicated to one service can’t be accessed by other services. This is one of the reasons
that makes it much easier to scale and understand from a whole end-to-end business aspect.
Picture a scenario where your databases have different needs or access requirements. The data
owned by one service may be largely relational, while a second service might be better served by a
NoSQL solution and a third service may require a vector database. In this scenario, using dedicated
services for each database could help you manage them more easily.
This structure also reduces coupling as one service can’t tie itself to the tables of another. Services
are forced to communicate via published interfaces. The downside is that dedicated databases
require a failure protection mechanism for events where communication fails.
A single shared database isn’t the standard for microservices architecture but bears mentioning as
an alternative nonetheless. Here, the issue is that microservices using a single shared database lose
many of the key benefits developers rely on, including scalability, robustness and independence.
Still, sharing a physical database may be appropriate in some situations. When a single database is
shared by all services, though, it’s very important to enforce logical boundaries within it. For
example, each service should own its have schema and read/write access should be restricted to
ensure that services can’t poke around where they don’t belong.
2. Saga pattern
The saga pattern is an alternative solution to other design patterns that allows for multiple
transactions by giving rollback opportunities.
1. Choreography:
Using the choreography approach, a service will perform a transaction and then publish an event. In
some instances, other services will respond to those published events and perform tasks according
to their coded instructions. These secondary tasks may or may not also publish events, according to
presets. In the example above, you could use a choreography approach so that each local
e-commerce transaction publishes an event that triggers a local transaction in the credit service.
2. Orchestration:
An orchestration approach will perform transactions and publish events using an object to
orchestrate the events, triggering other services to respond by completing their tasks. The
orchestrator tells the participants what local transactions to execute.
Saga is a complex design pattern that requires a high level of skill to successfully implement.
However, the benefit of proper implementation is maintained data consistency across multiple
services without tight coupling.
3. API gateway pattern
For large applications with multiple clients, implementing an API gateway pattern is a compelling
option One of the largest benefits is that it insulates the client from needing to know how services
have been partitioned. However, different teams will value the API gateway pattern for different
reasons. One of these possible reasons is because it grants a single entry point for a group of
microservices by working as a reverse proxy between client apps and the services. Another is that
clients don’t need to know how services are partitioned, and service boundaries can evolve
independently since the client knows nothing about them.
The client also doesn’t need to know how to find or communicate with a multitude of
ever-changing services. You can also create a gateway for specific types of clients (for example,
backends for frontends) which improve ergonomics and reduce the number of roundtrips needed to
fetch data. Plus, an API gateway pattern can take care of crucial tasks like authentication, SSL
termination and caching, which makes your app more secure and user-friendly.
Another advantage is that the pattern insulates the client from needing to know how services have
been partitioned. Before moving onto the next pattern, there’s one more benefit to cover: Security.
The primary way the pattern improves security is by reducing the attack surface area. By providing
a single entry point, the API endpoints aren’t directly exposed to clients and authorization and SSL
can be efficiently implemented.
Developers can use this design pattern to decouple internal microservices from client apps so a
partially failed request can be utilized. This ensures a whole request won’t fail because a single
microservice is unresponsive. To do this, the encoded API gateway utilizes the cache to provide an
empty response or return a valid error code.
An aggregator design pattern is used to collect pieces of data from various microservices and
returns an aggregate for processing. Although similar to the backend-for-frontend (BFF) design
pattern, an aggregator is more generic and not explicitly used for UI.
To complete tasks, the aggregator pattern receives a request and sends out requests to multiple
services, based on the tasks it was assigned. Once every service has answered the requests, this
design pattern combines the results and initiates a response to the original request.
This pattern is usually applied between services that are communicating synchronously. A
developer might decide to utilize the circuit breaker when a service is exhibiting high latency or is
completely unresponsive. The utility here is that failure across multiple systems is prevented when
a single microservice is unresponsive. Therefore, calls won’t be piling up and using the system
resources, which could cause significant delays within the app or even a string of service failures.
Implementing this pattern as a function in a circuit breaker design requires an object to be called to
monitor failure conditions. When a failure condition is detected, the circuit breaker will trip. Once
this has been tripped, all calls to the circuit breaker will result in an error and be directed to a
different service. Alternatively, calls can result in a default error message being retrieved.
There are three states of the circuit breaker pattern functions that developers should be aware of.
These are:
1. Open: A circuit breaker pattern is open when the number of failures has exceeded the
threshold. When in this state, the microservice gives errors for the calls without
executing the desired function.
2. Closed: When a circuit breaker is closed, it’s in the default state and all calls are
responded to normally. This is the ideal state developers want a circuit breaker
microservice to remain in — in a perfect world, of course.
3. Half-open: When a circuit breaker is checking for underlying problems, it remains in a
half-open state. Some calls may be responded to normally, but some may not be. It
depends on why the circuit breaker switched to this state initially.
A developer might use a command query responsibility segregation (CQRS) design pattern if they
want a solution to traditional database issues like data contention risk. CQRS can also be used for
situations when app performance and security are complex and objects are exposed to both reading
and writing transactions.
The way this works is that CQRS is responsible for either changing the state of the entity or
returning the result in a transaction. Multiple views can be provided for query purposes, and the
read side of the system can be optimized separately from the write side. This shift allows for a
reduction in the complexity of all apps by separately querying models and commands so:
● The write side of the model handles persistence events and acts as a data source for the
read side
● The read side of the model generates a projections of the data, which are highly
denormalized views
7. Asynchronous messaging
If a service doesn’t need to wait for a response and can continue running its code post-failure,
asynchronous messaging can be used. Using this design pattern, microservices can communicate in
a way that’s fast and responsive. Sometimes this pattern is referred to as event-driven
communication.
To achieve the fastest, most responsive app, developers can use a message queue to maximize
efficiency while minimizing response delays. This pattern can help connect multiple microservices
without creating dependencies or tightly coupling them. While there are tradeoffs one makes with
async communication (such as eventual consistency), it’s still a flexible, scalable approach to
designing a microservices architecture.
8. Event sourcing
The event sourcing design pattern is used in microservices when a developer wants to capture all
changes in an entity’s state. Using event stores like Kafka or alternatives will help keep track of
event changes and can even function as a message broker. A message broker helps with the
communication between different microservices, monitoring messages and ensuring
communication is reliable and stable. To facilitate this function, the event sourcing pattern stores a
series of state-changing events and can reconstruct the current state by replaying the occurrences
of an entity.
Using event sourcing is a viable option in microservices when transactions are critical to the
application. This also works well when changes to the existing data layer codebase need to be
avoided.
9. Strangler
Developers mostly use the strangler design pattern to incrementally transform a monolith
application to microservices. This is accomplished by replacing old functionality with a new service
— and, consequently, this is how the pattern receives its name. Once the new service is ready to be
executed, the old service is “strangled” so the new one can take over.
To accomplish this successful transfer from monolith to microservices, a facade interface is used by
developers that allows them to expose individual services and functions. The targeted functions are
broken free from the monolith so they can be “strangled” and replaced.
To fully understand this specific pattern, it’s helpful to understand how monolith applications
differ from microservices.
10. Decomposition patterns: Decomposition design patterns are used to break a monolithic
application into smaller, more manageable microservices. A developer can achieve this in one of
three ways:
1. Decomposition by business capability:
Many businesses have more than one business capability. For example, an e-commerce store is
likely to have capabilities that include managing product catalogs, inventory, orders, and delivery. A
single monolithic application might have been used for every service in the past, but say, for
example, the business decides to create a microservices application to manage these services
moving forward. In this common scenario, the business might choose to use decomposition by
business capability.
This may be used when an application has a large number of interrelated functions or processes.
Developers may also use it when functions or processes are likely to change frequently. The benefit
is that having more focused, smaller services allows for faster iterations and experimentation.
2. Decomposition by subdomain:
This is well suited for exceptionally large and complex applications that utilize a lot of business
logic. For example, you might use this if an application uses multiple workflows, data models and
independent models. Breaking the application into subdomains helps make managing the
codebase easier while facilitating faster development and deployment. An easy-to-grasp example
is a blog that’s hosted on a separate subdomain (for instance, blog.companyname.com). This
approach can separate the blog from the root domain’s business logic.
3. Decomposition by transaction:
This is an appropriate pattern for many transactional operations across multiple components or
services. Developers could choose this option when there are strict consistency requirements. For
example, consider cases where an insurance claim is submitted. The claim request might interact
with both a Customers application and Claims microservices at the same time.
Setting up the proper architecture and process tooling will help you create a successful microservice
workflow. Use the design patterns described above and learn more about microservices in our blog
to create a robust, functional app.
Hierarchy of Java Exception classes
The java.lang.Throwable class is the root class of Java Exception hierarchy inherited by two
subclasses: Exception and Error. The hierarchy of Java Exception classes is given below:
● Exception: Most of the cases exceptions are caused by our program and these are
recoverable.
● Error: Most of the cases errors are not caused by our program these are due to lack of system
● Types Of Exception:
exceptions are checked at compile-time by the compiler. The compiler ensures whether the
programmer handles the exception or not. The programmer should have to handle the
● Unchecked Exceptions: The unchecked exceptions are just opposite to the checked
exceptions. The compiler will not check these exceptions at compile time. In simple words,
if a program throws an unchecked exception, and even if we didn’t handle or declare it the
program would not give a compilation error. Usually, it occurs when the user provides bad
Optional<T> is a container object which may or may not contain a non-null value of type T.
It was introduced in Java 8 in the java.util package to reduce the occurrence of NullPointerException
and to promote functional programming.
🧠 Intermediate-Level Questions
6. How is orElse() different from orElseGet()?
🔍 Advanced-Level Questions
11. How do you combine multiple Optionals?
Here is a table listing most (if not all) Java IO classes divided by input, output, being byte based or
character based, and any more specific purpose they may be addressing, like buffering, parsing
etc.
Utilities SequenceInputStream
1. What is the difference between java.util.Date and java.time.LocalDate?
java.time.LocalDate (Java 8+) is immutable, thread-safe, and only represents a date (no
time or timezone).
It was introduced in Java 8 to fix issues in the old Date and Calendar APIs.
Based on the Joda-Time library, it provides clear and immutable date-time handling.
3. How do you get the current date and time in Java 8+?
OffsetDateTime includes only the offset (like +05:30) without the full zone rules.
3. How do you calculate the difference between two dates?
LocalDate start = LocalDate.of(2025, 1, 1);
LocalDate end = LocalDate.of(2025, 5, 13);
Period diff = Period.between(start, end);
// diff.getMonths(), diff.getDays(), etc.
🔴 Advanced-Level Q&A
1. How does Java handle daylight saving time with ZonedDateTime?
Java automatically adjusts for DST when using ZonedDateTime and ZoneId. For instance,
America/New_York will show different offsets in summer vs winter.
7. How do you represent a machine timestamp, and what class would you use?
1. You're building a booking system — how would you store and display date/time across
different time zones?
Store: Always store in UTC using Instant or ZonedDateTime with UTC zone.
// Storing
ZonedDateTime utcTime = ZonedDateTime.now(ZoneOffset.UTC);
// Displaying
ZonedDateTime userTime = utcTime.withZoneSameInstant(ZoneId.of("Asia/Kolkata"));
This avoids confusion caused by daylight saving or regional differences.
2. How do you ensure your app handles leap years and time zone transitions correctly?
Use java.time API which automatically handles leap years and DST rules when using
ZonedDateTime.
3. If a server and client are in different time zones, how do you synchronize date-time
data?
Convert all date-time to UTC on the server before storage or transmission.
4. Have you ever encountered issues with time zone conversions? How did you solve them?
Sample
Yes, while working on an event scheduling system, we initially stored date-time without
time zone context using LocalDateTime, which led to mismatches during DST. We
switched to storing everything in ZonedDateTime with UTC and only converting to local
zones on the UI, which resolved the issue.
5. A user schedules a meeting at 10 AM New York time. How would you ensure it shows
correctly to a user in London?
ZonedDateTime londonTime =
nyTime.withZoneSameInstant(ZoneId.of("Europe/London"));
This handles time zone and DST automatically.
6. How would you calculate the number of business days between two dates?
Loop through the date range and count days excluding weekends:
A regular expression (regex) in Java is a sequence of characters that forms a search pattern.
It is used for pattern matching with strings. Java supports regex through the
java.util.regex package, primarily using Pattern and Matcher classes.
Pattern p = Pattern.compile("\\d+");
Matcher m = p.matcher("Age is 25");
System.out.println(m.find()); // true (25 is found)
System.out.println(m.matches()); // false (entire string doesn't match)
while (matcher.find()) {
System.out.println(matcher.group()); // 45, then 5
}
🔴 Hard Level
7. Write a regex to match a password that:
Has at least one digit
One lowercase
One uppercase
One special character
8. How do you use regex to replace multiple spaces with a single space?
^ – start of string
while (matcher.find()) {
System.out.println(matcher.group(2)); // openai.com, example.org
}
Logging in Java
The Java ecosystem offers a wide range of logging libraries, each with its strengths and trade-offs.
While this abundance gives developers flexibility, it can also lead to confusion when deciding which
framework best suits a project's needs.
This guide simplifies the decision-making process by comparing five popular Java logging libraries,
examining their core features, pros, and cons.
1. Log4j 2
Overview
Log4j 2 is a powerful and feature-rich logging framework that serves as a major improvement over
the original Log4j. It supports multiple configuration formats (XML, JSON, YAML, and properties)
and offers advanced features like asynchronous logging and plugin-based extensibility.
Key Features
Pros
● High performance.
● Flexible and extensible.
● Cross-language support (e.g., Python, Ruby, C#).
Cons
● Complex to configure.
● Steeper learning curve for beginners.
2. Logback
Overview
Logback is considered the successor to the original Log4j. It provides a clean API and is known for its
robust performance and advanced configuration capabilities.
Key Features
Pros
Cons
● Increased resource usage under heavy load and Can be complex for new developers.
3. SLF4J
Overview
The Simple Logging Facade for Java (SLF4J) is not a logging implementation itself but a facade that
allows the use of various logging backends like Logback or Log4j 2. It promotes flexibility and
decouples application code from specific logging frameworks.
Key Features
Pros
Cons
4. Tinylog
Overview
Tinylog is a lightweight and minimalistic logging framework aimed at simplicity and ease of use. It
has a small footprint and supports modern features like lambda expressions and lazy logging.
Key Features
Pros
Cons
Overview
java.util.logging is the default logging framework included with the Java Development Kit. It offers
basic logging capabilities and avoids the need for third-party dependencies.
Key Features
Pros
● No additional dependencies.
● Suitable for simple logging needs.
● Well-documented and supported by Oracle.
Cons
Final Thoughts
● For robust and enterprise-level features: Logback or Log4j 2 are strong candidates.
● For flexibility and abstraction: SLF4J allows easy migration between frameworks.
● For basic and dependency-free logging: Java Util Logging may suffice, though it's generally
considered less capable.
Regardless of your choice, adopting SLF4J as your logging API is a smart strategy to future-proof
your codebase and maintain flexibility in your logging backend.