Tuesday, August 19, 2025

Encapsulation in Java: Writing secure and maintainable code

# Encapsulation in Java: Writing secure and maintainable code

Encapsulation is one of the pillars of Object-Oriented Programming. It’s the foundation that makes your code secure, maintainable, and robust. But what exactly is encapsulation, and why should you care about it as a Software Developer?

What is Encapsulation?

Encapsulation is the practice of bundling data (attributes) and the methods (behavior) that operate on that data into a single unit (in this case, an object), while restricting direct access to the internal components. It is like a protective shield around your data that controls how it can be accessed and modified.

The key is: Hide the internal state and require all interactions to happen through well-defined interface.

Without encapsulation, your program becomes vulnerable to several issues:

  • Data corruption: Any part of your code could modify critical data inappropriately, and with this I don’t only mean hackers but even our fellow team members because the poor designed interface of the component
  • Inconsistent state: Objects might end up in invalid states that could cause horrendous errors
  • Debugging nightmares: When data can be modified anywhere, tracking down bugs becomes unbearable

A Banking Example

Let’s use a very simple use case, imagine we have to write software to handle bank accounts (yes! I know, yet another banking example 😂). Requirements are:

  • Bank Accounts must have an owner and a balance
  • Bank Accounts must maintain a history of all transactions performed on them
  • Users can deposit funds into Bank Accounts
  • Users can withdraw funds from Bank Accounts

Let’s see encapsulation at work with this simple, practical example:

Poor Encapsulation (Don’t do this, please!)

public class BadBankAccount {
    public String owner;
    public double balance;
    public List<String> transactions;

    public BadBankAccount(String owner, double initialBalance) {
        this.owner = owner;
        this.balance = initialBalance;
        this.transactions = new ArrayList<>();
    }
}

// Let's create an account, no issues here
BadBankAccount account = new BadBankAccount("John Doe", 1000.0);
// Oops! Negative balance allowed
account.balance = -500.0;
// Transaction history is lost!
account.transactions.clear();
// Invalid state
account.owner = null;

As you can see, this approach is problematic, because:

  • All internals are publicly accessible, which is not good
  • No control over what values can be set
  • No protection or access control to internal data, important information can be deleted!
  • No validation or business logic is enforced
  • No clear indication of how to use this component

With Encapsulation (not perfect, but better)

public class BankAccount {
    // Private fields - hidden from outside access
    private String owner;
    private double balance;
    private List<String> transactions;
    private static final double MINIMUM_BALANCE = 0.0;

    // Constructor with validation (This can be better implemented as a factory method)
    public BankAccount(String owner, double initialBalance) {
        if (owner == null || owner.trim().isEmpty()) {
            throw new IllegalArgumentException("Owner name cannot be null or empty");
        }
        if (initialBalance < MINIMUM_BALANCE) {
            throw new IllegalArgumentException("Initial balance cannot be negative");
        }

        this.owner = owner;
        this.balance = initialBalance;
        this.transactions = new ArrayList<>();
        addTransaction("Account opened with balance: $" + initialBalance);
    }

    // Controlled access to balance (read-only)
    public double getBalance() {
        return balance;
    }

    // Controlled access to owner (read-only)
    public String getOwner() {
        return owner;
    }

    // Safe way to view transactions (returns unmodifiable copy)
    public List<String> getTransactionHistory() {
        return Collections.unmodifiableList(transactions); 
    }

    // Business logic for deposits
    public boolean deposit(double amount) {
        if (amount <= 0) {
            System.out.println("Deposit amount must be positive");
            return false;
        }

        balance += amount;
        addTransaction("Deposited: $" + amount + " | New balance: $" + balance);
        return true;
    }

    // Business logic for withdrawals
    public boolean withdraw(double amount) {
        if (amount <= 0) {
            System.out.println("Withdrawal amount must be positive");
            return false;
        }

        if (balance - amount < MINIMUM_BALANCE) {
            System.out.println("Insufficient funds. Current balance: $" + balance);
            return false;
        }

        balance -= amount;
        addTransaction("Withdrew: $" + amount + " | New balance: $" + balance);
        return true;
    }

    // Private helper method - internal implementation detail
    private void addTransaction(String transaction) {
        String timestamp = java.time.LocalDateTime.now().toString();
        transactions.add(timestamp + " - " + transaction);
    }

    // Utility method for account summary
    public void printAccountSummary() {
        String str = String.format(
            "{ owner: %s, balance: %.2f, totalTransactions: %d }",
            owner, balance, transactions.size()
        );
        System.out.println(str);
    }
}

Using the Encapsulated Class

public class BankingDemo {
    public static void main(String[] args) {
        // Create account safely
        BankAccount account = new BankAccount("Alice Johnson", 1000.0);

        // All interactions go through controlled methods
        account.deposit(250.0);    
        account.withdraw(100.0);   
        account.withdraw(2000.0);  

        // Data access is safe and controlled
        System.out.println("Current balance: $" + account.getBalance());
        System.out.println("Account owner: " + account.getOwner());

        // Transaction history is safely accessible
        List<String> history = account.getTransactionHistory();
        System.out.println("\nTransaction History:");
        for (String transaction : history) {
            System.out.println(transaction);
        }
        // This will throw an exception, you cannot modify the list
        // history.clear();

        account.printAccountSummary();
    }
}

Achieving Encapsulation with Java

1. Access Modifiers

The first step to achieve encapsulation is to use correctly the access modifiers.

  • private: Only accessible within the same class
  • protected: Accessible within the same package and subclasses
  • public: Accessible from anywhere
  • Package-private (no modifier): Accessible within the same package

2. Getter Methods

Provide controlled read access to private data:

public String getName() {
    return name;
}

// For collections, return copies (and unmodifiable) to prevent external modification
public List<String> getItems() {
    return new ArrayList<>(items);
    // or
    return Collections.unmodifiableList(items); 
}

3. Setter Methods with Validation

Provide controlled write access with business logic:

public void setAge(int age) {
    if (age < 0 || age > 150) { // business rule
        throw new IllegalArgumentException("Age must be between 0 and 150");
    }
    this.age = age;
}

4. Immutable Objects

For some cases, make objects unchangeable after creation:

public final class ImmutablePerson {
    private final String name;
    private final int age;

    public ImmutablePerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    // No setters - object cannot be changed after creation
}

Best Practices for Encapsulation

  1. Always start by declaring everything private: Classes, fields, methods, always start by making them private, then as needed, make them protected or even public.
  2. Provide public methods only when needed: Don’t create getters/setters automatically
  3. Validate inputs: Always check parameters in public methods
  4. Return copies of mutable internal data or make it immutable: Prevent external modification of internal data
  5. Use meaningful method names: Methods should describe business operations, not just data access
  6. Keep internal logic private: Helper methods should be private, they only have meaning inside the class.

Conclusion

If you need a frontline defender against bugs, data corruption, and maintenance headaches, encapsulation should be part of your go-to strategy. The investment in designing good encapsulation pays dividends throughout the lifetime of your application.

There is a quote I read somewhere and after some research I found who said it:

“Make it easy to do right, and hard to go wrong” - Gretchen Rubin

Design your components with that quote in mind: Design your components to do the “right thing” easy and to do the “wrong thing” hard. Encapsulation is your friend here.

Wednesday, August 13, 2025

Administra correctamente tus cuentas Github personal y de trabajo


Es muy comun que como desarrollador software tengas proyectos en tu maquina donde tienes repositorios tanto con cuenta del trabajo como con cuenta personal. Propablemente ya te has topado con problemas como "Permisson denied (publickey)" cuanto tratas de hacer "push" de alguno de esos repositorios. Que puede estar pasando? seguramente alguna confusion en cuanto a las llaves SSH que usar.

Este problema lo tuve durante mucho tiempo con la maquina del trabajo, y mi forma de solucionarlo era constantemente reiniciar el agente ssh (ssh-agent) y cargar la llave publica SSH correcta con la que iba a trabajar en esa sesion en la terminal. Incluso, debido a esto, llegaba a hacer commit a repositorios del trabajo con mi cuenta personal, algo que no se debe de hacer.

Definitivamente esto no es lo mejor, y despues de investigar un rato, encontre una mejor solucion.

Alias para Host SSH

El primer paso es tener bien configurado el archivo ~/.ssh/config de la siguiente manera

-----------------------------------------------------------------------------
# Cuenta personal github
Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/llavePublicaPersonalSsh
    AddKeysToAgent yes
    UserKeychain yes

# Cuenta github del trabajo
Host github-work
    HostName github.com
    User git
    IdentityFile ~/.ssh/llavePublicaTrabajoSsh
    AddKeysToAgent yes
    UserKeychain yes
------------------------------------------------------------------------------

Esto crea 2 alias, uno para la cuenta personal y otra para del trabajo, y que estaran usando diferentes llaves publicas SSH, aunque como puedan notar, ambas esten apuntando al HostName github.com.

Configura .gitconfig

Podemos configurar git para que automaticamente use diferentes configuraciones basado en el directorio en que nos encontremos de la siguiente forma en el archivo ~/.gitconfig 

-----------------------------------------------------------------------------
[user]
    name = Tu nombre
    email = personal@email.com


[includeif "gitdir:~/Projects/Personal/"]
    path = ~/.gitconfig-personal

[includeif "gitdir:~/Projects/Work/"]
    path = ~/.gitconfig-work
-----------------------------------------------------------------------------

Despues el archivo ~/.gitconfig-personal

-----------------------------------------------------------------------------
[user]
    name = Tu nombre
    email = personal@email.com
[core]
    sshCommand = ssh -i ~/.ssh/llavePublicaPersonalSsh
-----------------------------------------------------------------------------

y por ultimo el archivo ~/.gitconfig-work

-----------------------------------------------------------------------------
[user]
    name = Tu nombre
    email = work@email.com
[core]
    sshCommand = ssh -i 
~/.ssh/llavePublicaTrabajoSsh
-----------------------------------------------------------------------------

que estamos logrando con esto? ah pues es muy facil de probar.

Si llegamos a crear proyectos git debajo del folder ~/Projects/Personal por default, la configuracion de user.name y user.email sera la definida en el archivo ~/.gitconfig-personal. Esto lo podemos verificar muy facilmente si seguimos los siguientes pasos:

  1. Crea un folder dentro de ~/Projects/Personal, digamos ~/Projects/Personal/ejemplo
  2. Dentro del folder inicia un repositorio de git con git init
  3. Verifica que el comando: git config user.name muestra tu nombre
  4. Verifica que el comando: git config user.email muestra tu correo personal
De manera similar con nuevos repositorios git dentro del folder ~/Projects/Work deberan mostrar tu nombre y correo del trabajo.

Cabe mencionar que esto aplicara a nuevos repositorios git que crees inicializandolos con git init. Para repositorios existentes o repositorios que tengas que clonar hay que hacer lo siguiente que menciono en este articulo.

Clona repositorios con el Host correcto

Para indicar a git que configuracion usar para el repositorio que estamos a punto de clonar debemos de indicarle el Host correcto en el comando "clone":

-----------------------------------------------------------------------------

cd 
~/Projects/Work/Repositories

git clone git@github-work:company/some-interesting-project.git

-----------------------------------------------------------------------------

Notaran en rojo que el host especificado no es github.com, si no github-work, que es el alias que especificamos antes en el archivo ~/.ssh/config . Esto le dice a git que alias usar, por consiguiente que llave SSH usar tambien y al estar debajo del folder ~/Projects/Work usara la correcta configuracion de user.name y user.email para repositorios del trabajo.

Actualiza repositorios exsitentes

Todos los pasos anteriores no funcionaran para repositorios que ya tengamos clonados en los correspondientes folders. Entonces, para lograr lo mismo que ya hablamos anteriormente, tendremos que actualizar las remote URL de cada uno de estos repositorios. Afortunadamente no es un paso dificil, basicamente, tenemos que obtener las remote URL y reasignarlas usando el Host alias correcto.

-----------------------------------------------------------------------------

# Entra al folder
cd ~/Projects/Work/Repositories/interesting-project-already-cloned

# Ve que remote URLs hay
git remote -v

# Actualiza
git remote set-url origin git@github-work:company/interesting-project.git

# Verifica que las remote URL de origin cambiaron
git remote -v

-----------------------------------------------------------------------------

Asegurate de correr el agente SSH y cargar las llaves

Tal vez el ultimo paso sea asegurarnos de que cada que entremos a una sesion en la termina el agente SSH este corriendo y tenga las llaves correctas cargadas agregando la siguientes instrucciones a tu archivo ~/.zshrc~/.bashrc segun sea tu caso:

-----------------------------------------------------------------------------

# Inicia el agente SSH
eval "$(ssh-agent -s)" > /dev/null

# Agrega la llave privada personal
ssh-add ~/.ssh/llavePublicaPersonalSsh /dev/null

# Agrega la llave privada del trabajo
ssh-add ~/.ssh/
llavePublicaTrabajoSsh /dev/null

-----------------------------------------------------------------------------

y listo! con esto deberia quedar.

Installing Erlang and Elixir on Mac (Sequoia)

I was trying to install Erlang and Elixir on my Mac and when running:

KERL_CONFIGURE_OPTIONS="--without-javac --with-ssl=$(brew --prefix openssl@3)" asdf install erlang 27.3.4.1

I got the following error:

checking for OpenSSL in /opt/homebrew/opt/openssl@3... configure: error: neither static nor dynamic crypto library found in /opt/homebrew/opt/openssl@3 ERROR: /Users/rafael.gutierrez/.asdf/plugins/erlang/kerl-home/builds/asdf_27.3.4.1/otp_src_27.3.4.1/lib/crypto/configure failed!

Reading the following link, it makes me wonder what was the current architecture setup in the terminal

So running the following you can know what is the architecture of the current bash process:

uname -m

In my case, it was: x86_64. So I tried to force the execution of zsh under the arm64 architecture with:

env /usr/bin/arch -arm64 /bin/zsh --login

  • env - Runs commands in a clean environment
  • /usr/bin/arch - is a mac utility to run commands under specific architecture
  • -arm64 - the architecture
  • /bin/zsh --login - start a login shell with zsh
after that command if you can verify again with "uname -m".

Then I tried to run again the installation and it worked fine.