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 o ~/.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.


Wednesday, July 16, 2025

A Simple Agent to summarize web content using Embabel

AI (Artificial Intelligence) is everywhere we go a

AI (Artificial Intelligence) is everywhere we go and definitely it is here to stay.

A great use of AI for us as Software Developers is the creation of Intelligent Agents that, with the help of Large Language Models (LLMs), can solve problems that would be complex or impossible to address through traditional programming.

A few weeks ago I learned about the existence of a new framework that Rod Johnson (creator of the Spring framework) and other people are working on called Embabel

Embabel is a framework for creating agent workflows in the JVM by mixing interactions with LLMs via prompts and code with domain models (Java/Kotlin classes). The framework is built on top of Spring AI

The framework is relatively new and still in development, there is no official documentation yet and it’s possible that some things I explain here will change in the future (although I don’t think it will change radically).

The code for this example can be found in my Github repository at: abadongutierrez/basic-embabel-agent

Use Case: Web content summarization

Almost all of us have used LLMs to summarize some text. In fact, summarization is one of the great uses of LLMs, and in today’s example we’ll use Embabel to create an Agent that extracts content from websites we indicate and summarizes their content.

In general, we’ll use Embabel to build an agent that:

  1. Receives free text input from the user (via Spring Shell).
  2. Extracts web links mentioned by the user (with the help of LLM’s)
  3. Visits each site, obtains its content in the form of free text without HTML tags (using Tools).
  4. Generates a summary of each site’s content (again, using a LLM).

To visit each link and extract the content from that website, we’ll use the JSoup library. With this library we can easily connect to a website and extract only the text without HTML tags as follows:

// Connect and get the HTML document
Document doc = Jsoup.connect("https://en.wikipedia.org/").get();
// get only the text (no HTML tags)
doc.text();

How the Agent is created?

To define an Agent we need to create a class annotated with @Agent. This is very similar to using @Component and the derived annotations that exist in the Spring Framework. In fact, @Agent also derives from @Component so it’s managed as a Bean and, therefore, we can take advantage of dependency injection.

@Agent(description = "Agent to summarize content of web pages")  
public class SummarizingAgent {
    @Action  
    public WebPageLinks extractWebPagesLinks(UserInput userInput) { ... }

    @Action
    public SummarizedPages summarizeWebPages(WebPageLinks webPageLinks, OperationContext operationContext) { ... }

    @AchievesGoal(description = "Show summarized content of the web pages to the user")  
    @Action  
    public SummarizedPages showSummarization(SummarizedPages summarizedPages) { ... }
}

It’s important to assign a good description to the agent, since when interacting with them through Spring Shell, Embabel uses an LLM to select which agent will respond to the user’s request. This selection is based on an analysis of the user’s intention and correspondence with the most suitable agent to handle it.

Each method that represents a step in the agent’s flow must be annotated with @Action. The method that represents the agent’s final goal is also annotated with @AchievesGoal.

When we interact with Agents via the Spring Shell interface, generally the first step is the @Action method that receives a UserInput as an argument. I mention this because using Agents via Spring Shell is not the only way to interact with them - you can also use other mechanisms that I’ll try to explore in future posts.

Agent Flow

There is no way to specify the Agent flow programmatically. The framework, as stated on the homepage, tries to go beyond simply specifying a flow through a state machine and applies intelligent planning at the beginning of the flow and after the execution of each step. The framework detects the flow through the relationship between methods by inspecting the data types in “inputs” (method arguments) and “outputs” (return type).

SummarizingAgent

In this tutorial we create the SummarizingAgent which as a first step extracts URLs from user input. To achieve this we use an LLM because since user instructions are free text without format, LLMs are good at analyzing text and extracting information that we indicate. This is implemented in the extractWebPagesLinks method.

@Action  
public WebPageLinks extractWebPagesLinks(UserInput userInput) {  
    String prompt = String.format("""  
            Extracts the urls from the provided user input.

            <user-input>  
            %s
            </user-input>

            Extract only the links mentioned in the user input, dont add any other links.  
            """.trim(), userInput.getContent());  
    return PromptRunner.usingLlm().createObjectIfPossible(prompt, WebPageLinks.class);  
}

The second step in the flow is to extract the text content from each website and here we again rely on an LLM to obtain a summary of that content. This is implemented in the summarizeWebPages method. This method has 2 ways of acting and this depends on the app.useOpenAI flag defined in application.properties. This small application is designed to use Ollama and local models llama3.2 and all-minilm but you can also use OpenAI by setting the app.useOpenAI flag to true, which means you need to specify the OPENAI_API_KEY environment variable for the application to work correctly.

I implemented the summarizeWebPages method in 2 ways because llama3.2 is not as powerful a model as OpenAI models and I had many issues using the same prompt. So when using llama3.2 I used a different prompt and also an alternative in case the first prompt failed.

...
@Value("${app.useOpenAI:false}") boolean useOpenAI

...

@Action  
public SummarizedPages summarizeWebPages(WebPageLinks webPageLinks, OperationContext operationContext) {  
    if (this.useOpenAI) {  
        return getSummarizedPagesUsingOpenAI(webPageLinks);  
    }  
    return getSummarizedPagesUsingLocalModels(webPageLinks, operationContext);  
}

The last step in the flow is simply to return the set of pages and their summary so that the framework prints it to the Spring Shell console. This is implemented in the showSummarization method which must also be marked with the @AchievesGoal annotation because once this method is executed, the agent’s goal will have been achieved.

@AchievesGoal(description = "Show summarized content of the web pages to the user")  
@Action  
public SummarizedPages showSummarization(SummarizedPages summarizedPages) {  
    return summarizedPages;  
}

Interaction with LLMs

The framework has the concept of PromptRunners which, as their name indicates, execute a prompt to an LLM.

A PromptRunner has methods to execute a prompt and convert the prompt output to a domain object with methods like createObjectIfPossible or createObject. This gives the advantage of applying strong typing in our programs and thus being able to use refactoring techniques more easily.

The framework defines certain LLMs that programs will use by default. In our case we’re using local models with Ollama so in the application.properties file we can find the following properties that indicate which models will be used by default:

embabel.models.default-llm=llama3.2:latest  
embabel.models.default-embedding-model=all-minilm:latest  
embabel.models.embedding-services.best=all-minilm:latest  
embabel.models.embedding-services.cheapest=all-minilm:latest  
embabel.models.llms.best=llama3.2:latest  
embabel.models.llms.cheapest=llama3.2:latest  

embabel.agent-platform.ranking.llm=llama3.2:latest

When performing operations with LLMs, one of the main things that must be specified are the prompts, but PromptRunners also provide the facility to specify the “Tools” we want to use as part of executing a prompt. In this small application we are specifying and using JSoup as a “Tool” to extract text from a website.

“Tools” can be specified using the Spring AI annotation @Tool and this can be seen implemented in the JSoupTool class which is also a Spring Bean that we easily inject into the Agent.

@Component  
public class JSoupTool {
    ...

    @Tool(name = "jsoup", description = "A tool to extract text from web pages using JSoup")  
    public String getPageTextTool(String url) {  
        ...
    }

PromptRunners use the LLMs specified by default or using LlmOptions you can specify different models. In the case of this application we use this functionality to specify the OpenAI model to use when the app.useOpenAI flag is active.

String prompt = " ... ";
BuildableLlmOptions llmOptions = LlmOptions.fromCriteria(  
        ModelSelectionCriteria.byName("gpt-4.1-mini")  
);  
return PromptRunner  
        .usingLlm(llmOptions)  
        .withToolObject(jSoupTool)  
        .createObjectIfPossible(prompt, SummarizedPages.class);

Execution of the program

This application is configured to run using Spring Shell as an interface and we can notice this by the @EnableAgentShell annotation in the class where the main method is located.

@SpringBootApplication  
@EnableAgents(  
       loggingTheme = LoggingThemes.STAR_WARS,  
       localModels = {LocalModels.OLLAMA}  
)  
@EnableAgentShell
public class BasicEmbabelAgentApplication {  
    public static void main(String[] args) {  
       SpringApplication.run(BasicEmbabelAgentApplication.class, args);  
    }  
}

Also as you can notice it’s configured to search for and use local LLMs using Ollama.

Once the application is executed, the Spring Shell prompt appears where we can use the x (execute) command to indicate the “user input” and make the Embabel agent platform search for and select the appropriate agent to handle the user’s request:

...
21:05:55.957 [main] INFO  DelegatingAgentScanningBeanPostProcessor - All deferred beans were post-processed.
21:05:55.958 [main] INFO  BasicEmbabelAgentApplication - Started BasicEmbabelAgentApplication in 1.834 seconds (process running for 2.074)
Fear is the path to the dark side.
starwars> x "summarize the content of the following page https://en.wikipedia.org/wiki/Alan_Turing"

output:

You asked: UserInput(content=summarize the content of the following page https://en.wikipedia.org/wiki/Alan_Turing, timestamp=2025-07-08T03:15:04.089557Z)

{
  "summarizedPages" : [ {
    "url" : "https://en.wikipedia.org/wiki/Alan_Turing",
    "summary" : "Alan Turing (1912-1954) was a British mathematician, computer scientist, logician, philosopher, and cryptographer who made significant contributions to the development of computer science, artificial intelligence, and cryptography.\n\n**Early Life and Education**\n\nTuring was born on June 23, 1912, in London, England. He studied mathematics at King's College, Cambridge, where he graduated with a First Class Honours degree in Mathematics. During World War II, Turing worked at the Government Code and Cypher School (GC&CS) at Bletchley Park, where he played a crucial role in cracking the German Enigma code.\n\n**Contributions to Computing**\n\nTuring is considered one of the founders of computer science. He proposed the theoretical foundations of modern computer science, including:\n\n1. **The Turing Machine**: a mathematical model for a computer's central processing unit (CPU).\n2. **The Universal Turing Machine**: a machine that could simulate any other machine.\n3. **Computability Theory**: the study of what can be computed by a machine.\n\n**Codebreaking and Cryptography**\n\nAt Bletchley Park, Turing worked with a team to crack the Enigma code, which was used by the German military during World War II. His work significantly contributed to the Allied victory.\n\n**Personal Life and Later Years**\n\nTuring's personal life was marked by tragedy. In 1952, he was convicted of gross indecency for his relationship with a man, which led to his chemical castration and eventual death in 1954 at the age of 41.\n\n**Legacy**\n\nTuring's legacy is profound:\n\n1. **Computer Science**: Turing's work laid the foundation for modern computer science.\n2. **Artificial Intelligence**: His ideas on machine intelligence and computation have influenced AI research.\n3. **Cryptography**: Turing's contributions to codebreaking and cryptography have had a lasting impact on national security.\n\n**Recognition**\n\nIn 2009, the British government officially apologized for Turing's treatment and posthumously pardoned him. In 2017, he was featured on the £50 note, making him the first openly gay person to be featured on a British banknote.\n\nTuring's life and work serve as a testament to his innovative spirit and contributions to science and society. His legacy continues to inspire new generations of computer scientists, mathematicians, and thinkers."
  } ]
}

If you carefully look at the “logs” that the application prints when executing, you’ll notice the steps that Embabel takes to select the Agent to execute, the actions/goals that the Agent contains, and the planning it does for executing the “actions” after the execution of each step.

Conclusion

Although Embabel is still in a development stage, it already demonstrates being a promising proposal for developers working on the JVM. Embabel is developed in Kotlin but, as Rod Johnson has mentioned in interviews, it should be able to be used naturally in Java as can be seen in the code of this example.

Its declarative approach allows creating intelligent agents using annotations, without defining flows explicitly. Instead, an AI algorithm (without using LLMs) infers the execution plan according to the agent’s context and after executing each step. Additionally, it integrates natively with known technologies like Spring and Spring AI, which facilitates its adoption. It also includes support for unit and integration testing, making it suitable for serious projects from the start.