Open-Closed Principle – Simple Explanation with Example

When building applications, we often need to add new features or change behavior. A common mistake is modifying existing code every time we need something new.

This can easily introduce bugs and break parts of the system that were already working.

The Open-Closed Principle (OCP) helps solve this problem.

Single Responsibility Principle

What is Open-Closed Principle?

The idea is simple:

Software entities should be open for extension, but closed for modification.

That means:

  • You should be able to add new behavior
  • Without changing existing code

Why This Matters

When we modify existing code:

  • We risk breaking existing functionality
  • Testing becomes harder
  • The code becomes less stable over time

Instead, we should extend the code in a safe way.

Bad Example (Modifying Existing Code)

Let’s say we have a class that calculates discounts:


class DiscountService {
    calculate(price, type) {
        if (type === "regular") {
            return price * 0.1;
        } else if (type === "premium") {
            return price * 0.2;
        }
    }
}

Problem here:

  • Every time we add a new discount type, we must modify this class
  • This can break existing logic

Good Example (Open for Extension)

Instead of modifying the class, we can extend behavior:


// Base class
class Discount {
    calculate(price) {
        return 0;
    }
}

// Extended classes
class RegularDiscount extends Discount {
    calculate(price) {
        return price * 0.1;
    }
}

class PremiumDiscount extends Discount {
    calculate(price) {
        return price * 0.2;
    }
}

Now we use them like this:


function getDiscount(discount, price) {
    return discount.calculate(price);
}

Why this is better:

  • We don’t change existing code
  • We add new behavior using new classes
  • The system becomes more stable and flexible

Adding a new discount is easy:


class FestivalDiscount extends Discount {
    calculate(price) {
        return price * 0.3;
    }
}

No need to modify old code.

Going One Step Further (Better Design)

In real-world applications, we often combine OCP with dependency injection.


class CheckoutService {
    constructor(discount) {
        this.discount = discount;
    }

    getFinalPrice(price) {
        return price - this.discount.calculate(price);
    }
}

Now we can pass any discount type:


const discount = new PremiumDiscount();
const checkout = new CheckoutService(discount);

console.log(checkout.getFinalPrice(100));

This makes the system flexible and easy to extend.

Real-Life Analogy

Think of a mobile phone:

  • You don’t modify the phone to add new features
  • You install apps to extend functionality

The phone is closed for modification but open for extension.

Key Idea to Remember

  • Don’t modify existing working code
  • Extend behavior using new classes
  • Keep your system stable

Conclusion

At first, modifying existing code may seem easier. But as your project grows, it increases risk and complexity.

By following the Open-Closed Principle, your code becomes:

  • Easier to extend
  • Safer to maintain
  • More scalable


Single Responsibility Principle – Simple Explanation with Example

When writing code, it’s common to put multiple responsibilities inside a single file or class. It might work at first, but over time it becomes difficult to manage and update.

The Single Responsibility Principle (SRP) helps solve this.

It is one of the core ideas from SOLID and focuses on keeping code simple and maintainable.

Single Responsibility Principle

What is Single Responsibility Principle?

The idea is simple:

A class should have only one responsibility.

That means:

  • It should do one job
  • It should have only one reason to change

Why This Matters

When a class does too many things:

  • Changes become risky
  • Bugs become harder to track
  • Code becomes difficult to understand

Keeping responsibilities separate makes your code:

  • Cleaner
  • Easier to maintain
  • Easier to scale

Bad Example (Multiple Responsibilities in One Class)

Let’s look at a class that handles everything:


class UserService {
    validateUser(user) {
        if (!user.email) {
            console.log("Invalid user");
            return false;
        }
        return true;
    }

    saveUser(user) {
        console.log("Saving user to database");
    }

    sendEmail(user) {
        console.log("Sending welcome email");
    }

    processUser(user) {
        if (!this.validateUser(user)) return;

        this.saveUser(user);
        this.sendEmail(user);
    }
}

Problem here:

  • This class is handling validation
  • It is saving data
  • It is also sending emails

So if any of these responsibilities change, we must modify the same class.

Good Example (Single Responsibility per Class)

Now let’s split responsibilities into separate classes:


class UserValidator {
    validate(user) {
        return user.email ? true : false;
    }
}

class UserRepository {
    save(user) {
        console.log("Saving user to database");
    }
}

class EmailService {
    send(user) {
        console.log("Sending welcome email");
    }
}

Now we use them together:


class UserService {
    constructor() {
        this.validator = new UserValidator();
        this.repository = new UserRepository();
        this.emailService = new EmailService();
    }

    processUser(user) {
        if (!this.validator.validate(user)) return;

        this.repository.save(user);
        this.emailService.send(user);
    }
}

Why this is better:

  • Each class has only one responsibility
  • Changes in one part do not affect others
  • Code becomes easier to test and maintain

Going One Step Further (Better Design)

In real-world applications, we can improve this design even further.

Instead of validating data separately, we can ensure that a valid object is created from the beginning. This approach is often called "Parse, don't validate".

Let’s look at an improved version:


// User class ensures valid data during creation
class User {
    constructor(userData) {
        if (!userData.email) {
            throw new Error("Invalid User: Email is required.");
        }
        this.email = userData.email;
    }
}

// Handles only database operations
class UserRepository {
    save(user) {
        console.log(`Saving ${user.email} to the database...`);
    }
}

// Handles only email sending
class EmailService {
    sendWelcome(user) {
        console.log(`Sending welcome email to ${user.email}`);
    }
}

// Coordinates the flow
class UserRegistrationService {
    constructor(repository, emailService) {
        this.repository = repository;
        this.emailService = emailService;
    }

    register(userData) {
        try {
            const user = new User(userData);

            this.repository.save(user);
            this.emailService.sendWelcome(user);
        } catch (error) {
            console.error("Registration failed:", error.message);
        }
    }
}

Why this is better:

  • The User class ensures only valid data exists
  • The repository only handles saving data
  • The email service only handles sending emails
  • The registration service only coordinates the process

This design keeps responsibilities even more clearly separated and makes the system safer and easier to maintain.

Real-Life Analogy

Think of a restaurant:

  • Chef – cooks food
  • Cashier – handles billing
  • Delivery – delivers food

If one person does everything, it becomes messy.

Key Idea to Remember

  • One class → one responsibility
  • Keep responsibilities separate
  • Make changes easier and safer

Conclusion

At the beginning, combining everything in one place might feel faster. But as your project grows, it creates problems.

By following SRP, your code becomes:

  • Easier to read
  • Easier to modify
  • More organized

SOLID Principles – Simple Explanation for Beginners

 When I started learning object-oriented programming, I often wrote code that worked—but was hard to maintain or extend later.

That’s when I came across the SOLID principles.

These are five simple guidelines that help us write code that is:

  • Easy to understand
  • Easy to maintain
  • Easy to scale

In this blog, I’ll explain each principle in a simple way so you can understand the idea clearly.



What are SOLID Principles?

SOLID is a set of five design principles introduced by Robert C. Martin.

Each letter represents one principle:

  • S → Single Responsibility
  • O → Open-Closed
  • L → Liskov Substitution
  • I → Interface Segregation
  • D → Dependency Inversion

S — Single Responsibility Principle (SRP)

A class should have only one responsibility.

Simple idea

A class should do only one job.

If a class has multiple responsibilities:

  • It becomes harder to manage
  • Changes in one part can affect other parts

Example thinking

Instead of:

  • One class handling user data + logging + validation

Split it into:

  • One class for user data
  • One class for logging
  • One class for validation

Why it matters

It reduces bugs and makes code easier to update.


O — Open-Closed Principle (OCP)

Classes should be open for extension but closed for modification.

Simple idea

You should be able to add new features without changing existing code.

Why?

Changing existing code can:

  • Break other parts of the system
  • Introduce bugs

Better approach

Instead of modifying existing code:

  • Extend it using new classes or methods

L — Liskov Substitution Principle (LSP)

Child classes should be able to replace parent classes without breaking the program.

Simple idea

If a class extends another class, it should behave correctly in its place.

Example thinking

If:

  • Parent → returns a type of object

Then:

  • Child → should return the same type or a valid subtype

If not, it breaks the logic.


I — Interface Segregation Principle (ISP)

A class should not be forced to implement methods it does not need.

Simple idea

Don’t create large interfaces with unnecessary methods.

Better approach

Split large interfaces into smaller ones.

So each class only implements what it actually needs.


D — Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Simple idea

Don’t directly depend on concrete implementations.

Instead:

  • Use interfaces or abstractions

Why?

It makes your code:

  • Flexible
  • Easier to change
  • Less dependent on specific implementations

Final Thoughts

When I first learned these principles, they felt theoretical. But once I started applying them in small projects, I realized how useful they are.

They help you write code that is:

  • Cleaner
  • More organized
  • Easier to maintain



Encapsulation in Java – Simple Explanation for Beginners

When writing programs, one common issue is that data can be modified from different parts of the code without proper control. This often leads to unexpected bugs and makes applications harder to manage.

Encapsulation helps solve this problem.

It allows us to protect data and control how it is accessed, making our code more secure and easier to maintain.

In this blog, I will explain encapsulation in a simple way so that you can understand it clearly and start using it in your Java programs.



What is Encapsulation in Java?

In simple terms, encapsulation means wrapping data and methods together inside a single class.

Instead of allowing direct access to variables, we control how the data is accessed and modified through methods.

You can think of it as providing controlled access to the internal state of an object.

Why Encapsulation is Important

Data Hiding

Encapsulation hides the internal data from outside access. This prevents unwanted or incorrect changes.

Modularity

It helps break large programs into smaller, manageable parts (classes), making them easier to work with.

Flexibility and Maintainability

Encapsulation allows changes in the internal implementation without affecting other parts of the code.

This is especially useful when working on large applications.

How Encapsulation Works in Java

  • private variables
  • Getter methods
  • Setter methods

The idea is straightforward:

  • Keep data private
  • Allow access through methods

Example – BankAccount Class


class BankAccount {
    private String accountNumber;
    private double balance;

    public BankAccount(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        this.balance = initialBalance;
    }

    public String getAccountNumber() {
        return accountNumber;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        if (balance >= 0) {
            this.balance = balance;
        } else {
            System.out.println("Invalid balance.");
        }
    }
}

Explanation

  • The balance field is private, so it cannot be accessed directly from outside the class.
  • The getBalance() method allows reading the value.
  • The setBalance() method allows updating the value with validation.

This ensures controlled access to the data.

Adding More Operations


public void deposit(double amount) {
    if (amount > 0) {
        balance += amount;
    } else {
        System.out.println("Invalid deposit amount.");
    }
}

public void withdraw(double amount) {
    if (amount > 0 && amount <= balance) {
        balance -= amount;
    } else {
        System.out.println("Invalid withdrawal amount.");
    }
}

All operations are handled within the class, which prevents direct manipulation of data.

How to Use the Class


public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount("1234567890", 1000);

        System.out.println("Balance: " + account.getBalance());

        account.deposit(500);
        account.withdraw(300);

        System.out.println("Updated Balance: " + account.getBalance());
    }
}

In this example:

  • The balance is never accessed directly
  • All interactions happen through methods

Key Concept

  • Protecting data
  • Controlling access
  • Maintaining clean structure

Conclusion

Encapsulation is a fundamental concept in Java that helps build secure and maintainable applications. By using access modifiers and methods, we can ensure that data is accessed and modified in a controlled manner.

Understanding and applying encapsulation will help you write better structured and more reliable code.

Summary

  • Encapsulation combines data and methods in one class
  • Use private variables to restrict access
  • Use getters and setters for controlled access
  • Avoid direct access to class data
  • Improves security, flexibility, and maintainability

Abstraction in Java

When I started learning Java, abstraction felt confusing at first. But once I connected it with real-life examples, it started to make sense.

In this blog, I’ll explain abstraction in the simplest way possible—just like how I understood it.


What is Abstraction?

Abstraction means hiding the internal implementation details and showing only the essential functionality.

In my own words:

I focus on what something does, not how it does it.

This idea is actually everywhere in real life.

Real-Life Example (How I Understood It)

Think about a car:

  • I use the steering, brake, and accelerator
  • But I don’t know how the engine works internally

Still, I can drive the car perfectly.

That’s abstraction.

Why Abstraction is Important

When I started building projects, I realized abstraction helps me:

  • Reduce complexity
  • Write cleaner code
  • Hide sensitive logic
  • Make code reusable
  • Easily maintain large applications

How Abstraction is Achieved in Java

In Java, I mainly use two ways:

  • Abstract Classes
  • Interfaces

1. Abstract Class in Java

An abstract class is a class that I cannot create objects from directly. It can have both:

  • Abstract methods (no body)
  • Normal methods (with body)

Example

abstract class Vehicle {
    abstract void start();

    void stop() {
        System.out.println("Vehicle stopped");
    }
}

class Car extends Vehicle {
    void start() {
        System.out.println("Car starts with key");
    }
}

public class Main {
    public static void main(String[] args) {
        Vehicle v = new Car();
        v.start();
        v.stop();
    }
}

My Understanding

  • I define a general idea in Vehicle
  • Then provide actual behavior in Car
  • I can’t create Vehicle directly

2. Interface in Java

An interface is like a blueprint. It only defines methods, and I must implement them in another class.

Example


interface Payment {
    void pay();
}

class UPI implements Payment {
    public void pay() {
        System.out.println("Payment via UPI");
    }
}

class Card implements Payment {
    public void pay() {
        System.out.println("Payment via Card");
    }
}

public class Main {
    public static void main(String[] args) {
        Payment p1 = new UPI();
        p1.pay();

        Payment p2 = new Card();
        p2.pay();
    }
}

My Understanding

  • Same method pay()
  • Different implementations (UPI, Card)
  • Very flexible and reusable

Abstract Class vs Interface (Simple View)

Feature Abstract Class Interface
Methods Can have both abstract and normal methods Mostly contains abstract methods
Implementation Can provide method implementation Only method declaration (implementation in class)
Inheritance A class can extend only one abstract class A class can implement multiple interfaces
Usage Used when classes share common behavior Used for defining a common contract
Object Creation Cannot create object directly Cannot create object directly

Real Use Cases (Where I See This in Projects)

  • Payment systems → Same method, different payment types
  • Banking apps → Deposit/withdraw without knowing backend logic
  • APIs → Only necessary data is exposed
  • Frontend apps → Button click hides backend operations

Abstraction vs Encapsulation

I used to mix these up a lot:

  • Abstraction → Hides implementation
  • Encapsulation → Hides data

👉 Abstraction = Design
👉 Encapsulation = Security

Conclusion

Once I understood abstraction, I stopped worrying about internal complexity and started focusing on clean design.

If you’re a beginner, just remember:

Show only what is needed, hide everything else.

Understanding Polymorphism in Java (Simple & Beginner-Friendly Guide)

 When I first started learning Java, one concept that really stood out to me was polymorphism. At first, it sounded complex, but once I broke it down, it actually became one of the most powerful and easy-to-understand ideas in Object-Oriented Programming (OOP).

What is Polymorphism?

Polymorphism simply means “many forms.”

In Java, I think of it like this:

The same method name can behave differently depending on how I use it.

This helps me write cleaner, more flexible, and reusable code.

There are two main types of polymorphism in Java:

  • Compile-time Polymorphism (Method Overloading)
  • Run-time Polymorphism (Method Overriding)

1. Compile-time Polymorphism (Method Overloading)

What it means

When I create multiple methods with the same name but different parameters, it's called method overloading.

The difference can be:

  • Number of parameters
  • Type of parameters

Syntax


Example

Here’s a simple example I use to understand it better:

My Understanding

Even though the method name is the same (add), Java decides which method to call at compile time based on the arguments I pass.

That’s why it’s called static binding.

2. Run-time Polymorphism (Method Overriding)

What it means

When a child class provides its own version of a method that already exists in the parent class, it’s called method overriding.

Example

This is one of my favourite examples:


My Understanding

Here’s the interesting part:
  • I created an object of Dog
  • But referenced it using Animal
Even then, Java calls the Dog’s version of the method.

This decision happens at runtime, so it's called:
  • Dynamic Binding
  • Run-time Polymorphism


Final Thoughts

When I finally understood polymorphism, it changed how I write code. Instead of creating multiple method names, I can reuse the same method name and let Java handle the complexity.

If you're just starting out, don’t overthink it.
Just remember:
  • Overloading → Same method, different inputs
  • Overriding → Same method, different behavior in child class
Once this clicks, a lot of OOP concepts become much easier.



Oops Concepts : Inheritance In Java

When I first started learning Java, inheritance felt a bit confusing. But once I understood the basic idea, it became one of the easiest and most powerful concepts in Object-Oriented Programming (OOP).

In this blog, I’ll explain inheritance in very simple English, so even if you're a beginner student or aspiring developer, you can understand it easily. 

What is Inheritance in Java?

In simple words, inheritance means reusing code from another class.

I usually think of it like this:

  • A child inherits features from parents
  • Similarly, a class can inherit properties and methods from another class

Definition:

Inheritance is a mechanism where a child class gets properties and methods from a parent class.

Key Terms (Very Important)

Parent Class (Superclass) → The class that provides properties

Child Class (Subclass) → The class that inherits those properties

How Inheritance Works in Java

Java uses the keyword:

        extends

Basic Syntax:

This means the Child class can use everything from the Parent class.

Real Example (This Helped Me Understand)

In this example:

  • Animal is the parent class
  • Dog is the child class

What I learned from this:

  • The dog can use eat() from Animal
  • It can also use its own method bark()

This is called code reusability

Types of Inheritance in Java

1. Single Inheritance

This is the simplest type.

One child class inherits from one parent class

Example:

Parent → Child




2. Multilevel Inheritance

This works like a chain. Grandparent → Parent → Child


I found this useful when building layered applications.

3. Hierarchical Inheritance

Multiple child classes inherit from one parent
Parent
/ \
Child1 Child2



4. Multiple Inheritance (Using Interfaces)

Java does NOT support multiple inheritance using classes ❌ But it supports it using interfaces ✅


Advantages of Inheritance

From my experience, these are the biggest benefits:
  • Code Reusability → Write once, use many times
  • Less Code → No need to repeat logic
  • Easy Maintenance → Update in one place
  • Method Overriding → Customize behavior

Disadvantages of Inheritance

You should also know the downsides:
  • Can make code complex
  • Creates tight coupling
  • Harder to manage in large projects

Final Thoughts (My Advice)

When I started, I tried to learn everything at once—and got confused. What worked for me:
  • Start with simple examples
  • Practice small programs
  • Then move to advanced concepts

Stop Storing JWTs in LocalStorage: A 2026 Guide to MERN Auth




If you’re still putting your JSON Web Tokens (JWTs) into localStorage in your MERN apps, it's time to stop. As someone who’s spent way too many nights debugging broken auth flows and dealing with security audits, I’ve learned the hard way that localStorage is essentially an open invitation for XSS (Cross-Site Scripting) attacks to hijack your user sessions.

 In 2026, the standard has shifted. Here is how we’re handling authentication in the MERN stack now.

The Problem: LocalStorage is a Vulnerability 

When you store a token in localStorage, any JavaScript running on your page—including a compromised third-party package or an injected script—can read that token. If a hacker manages to execute just one line of code in your app, they have your user's identity. 

  The Modern Shift: Cookies are Your Best Friend

The gold standard now is to move tokens out of the reach of your client-side JavaScript by using HttpOnly, Secure, SameSite=Strict cookies. Because these cookies are not accessible to JavaScript, even if an attacker manages to run malicious code, they cannot scrape your auth tokens. 

How We’re Implementing "Refresh Token Rotation"

 Instead of a single, long-lived access token, we use a two-tiered system: 

Access Token: Short-lived (e.g., 15 minutes), stored in memory on the client.
Refresh Token: Long-lived (e.g., 7 days), stored in an HttpOnly cookie.

Every time the access token expires, your frontend makes a request to your Node.js backend using the refresh token cookie. If the refresh token is valid, the server rotates it (issues a new one) and invalidates the old one. If the refresh token has already been used—a red flag for token hijacking—the entire session is invalidated immediately.

Addressing the "Logout" GapOne of the biggest complaints I hear from developers switching to cookies is, "How do I clear the session on logout?" It’s simpler than you think: you tell the server to clear the cookie by setting its expiration date in the past, and your frontend clears the access token from its memory state.

Why This Matters Authentication isn't just a "set-and-forget" feature of your MERN stack. It’s an evolving security layer. By moving to cookie-based, rotated refresh tokens, you aren't just making your app more secure—you're aligning with modern industry standards that prevent the most common exploits.

Setting Up MinGW on Windows: A Beginner’s Guide




Introduction

MinGW (Minimalist GNU for Windows) is a popular compiler for C, C++, and other languages that provides the necessary tools for compiling and running code on Windows. In this guide, I’ll walk you through the steps to install and configure MinGW on your system. Whether you’re just getting started or need to configure your environment, follow these simple steps!

Step 1: Download MinGW

First, download the MinGW zip file from the following link:

👉 Download MinGW

Once downloaded, you’ll see a file like this:

Step 2: Extract the ZIP File

Now that you’ve downloaded the ZIP file, it’s time to extract it.

  1. Right-click on the zip file.
  2. Choose Extract All.
  3. Extract the contents to a folder on your desktop or in any other location.

You should see a folder named mingw-w64-bin_x86_64-mingw_20111101_sezero.

Step 3: Move MinGW64 Folder to C: Drive

After extracting, move the mingw64 folder to your C: drive. Here’s how:

  1. Open File Explorer and go to the folder where you extracted the ZIP file.
  2. Select the mingw64 folder.
  3. Right-click and choose Cut.
  4. Navigate to This PC > C: drive.
  5. Right-click in the C: drive and choose Paste.

The mingw64 folder should now be located directly in the C: drive:

Step 4: Adding MinGW to Environment Variables

Now, we need to configure your system so that you can use MinGW from any command line. There are two ways to do this:

Option 1: Directly Add MinGW to Path

  1. Press Windows + S and type Environment Variables.
  2. Select Edit the system environment variables.
  3. In the new window, click Environment Variables.
  4. Under the System variables section, scroll down and select Path. Then click Edit.
  5. In the Edit window, click New and paste the path to the mingw/bin folder (it will look like this):
C:\mingw64\bin

6. Press OK to close all windows.

Option 2: Use a User Variable (Recommended)

Instead of adding the path directly, you can create a user variable. This makes it easier to update your MinGW setup in the future. Here’s how:

  1. Press Windows + S and search for Environment Variables.
  2. Click on Edit the system environment variables.
  3. In the new window, click Environment Variables.
  4. Under the User variables section, click New to create a new user variable.
  • In the Variable name field, type MINGW_HOME.
  • In the Variable value field, paste the path to your MinGW folder, for example:

C:\mingw64


5. Press OK to create the variable.

6. Now, under System variables, scroll down and select Path.

7. Click Edit.

8. In the Edit window, click New and add the following line: 

%MINGW_HOME%\bin

This tells the system to use the path stored in the MINGW_HOME variable and look for the bin folder inside it.

9. Press OK to save everything.

Step 5: Verify the Installation

To make sure everything is working correctly:

  1. Press Windows + R, type cmd, and press Enter.
  2. In the Command Prompt window, type gcc --version and press Enter.

If MinGW is installed correctly, you should see the version information for gcc, which indicates that the compiler is now ready to use.

Conclusion

Congratulations! 🎉 You’ve successfully installed and configured MinGW on your Windows machine. You now have the flexibility to use it for compiling C and C++ programs, either by adding it directly to the system path or by using a user variable for easier future maintenance.

If you have any questions or run into any issues, feel free to drop a comment. Happy coding!

Interface Segregation Principle – Simple Explanation with Example

When building applications, we often create classes or interfaces that contain many different methods. At first, this may seem convenient ...