programacion-imperativa-vs-declarativa

Imperative and Declarative Programming: Differences

  • 6 min

Imperative programming describes how to obtain a result through instructions, while declarative programming expresses what result we want and leaves the concrete steps to another layer. These two approaches constantly coexist in software.

When we start programming, almost all of us learn by writing sequences of instructions that modify the computer’s memory step by step. It’s natural, because it resembles the hardware operation based on the von Neumann architecture.

As software grows, managing every detail of the how can be error-prone. This doesn’t mean abandoning the imperative, but rather using declarative abstractions when they better express the intention.

Today we will dissect these two approaches with a greater technical depth, analyzing how they manage state, flow control, and side effects.

Imperative Programming: Flow Control

Imperative programming focuses on describing how to achieve a result. The programmer dictates step by step the state changes needed to compute the solution.

Technically, it is based on three pillars:

  • Statements: commands that execute an action.
  • State mutation: variables that change value over time.
  • Explicit control structures: for loops, while loops, and if/else conditionals.

It is the paradigm of C (original), C++ (classic), Java (pre-8), and the foundation of C#.

Imperative Example in C#

Suppose we want to filter a list of numbers to get only the even ones and then square them.

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
List<int> results = new List<int>(); // 1. Mutable external state

// 2. Explicit control flow (How to iterate)
for (int i = 0; i < numbers.Count; i++)
{
    // 3. Conditional control flow (How to filter)
    if (numbers[i] % 2 == 0)
    {
        int square = numbers[i] * numbers[i];

        // 4. Side effect: Mutation of the results list
        results.Add(square);
    }
}

// results: { 4, 16, 36 }
Copied!

The code is straightforward, but it contains more mechanical details: we manage the index i, the results collection, and the stopping condition. The intention (“squared even numbers”) is mixed with the loop’s “plumbing.” Furthermore, this version cannot be parallelized without changing how we write to results.

Declarative Programming: Logical Abstraction

Declarative programming focuses on describing what we want to obtain, delegating flow control to the underlying language or framework.

Instead of specifying the traversal, we compose expressions that describe the transformation. Some declarative styles, such as functional programming, also favor:

  • Immutability: creating new values instead of modifying existing ones.
  • Higher-order functions: functions that receive or return other functions, such as Map, Filter, or Reduce.
  • Side effect control: keeping pure calculations and operations that modify the outside world separate.

These properties do not define all declarative programming. SQL, for example, is declarative even though a query can end up modifying data.

Declarative Example in C# (LINQ)

Let’s see the same problem solved declaratively using LINQ (Language Integrated Query).

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };

// "Tell me WHAT you want":
// You want numbers WHERE they are even, SELECTING their square.
var results = numbers
    .Where(n => n % 2 == 0)   // Filter (Declarative)
    .Select(n => n * n)       // Transformation (Projection)
    .ToList();                // Materialization
Copied!

Here there are no for loops, no i indices, or explicit if statements. We are building a data pipeline.

  • Where does not traverse the collection immediately; it builds a query with deferred evaluation.
  • The code shows the intention without exposing the index or the loop.
  • We can evaluate whether PLINQ (AsParallel) provides performance, although parallelization is not free and requires operations to be safe and have enough workload to offset the cost.

Technical Comparison

Why is the world moving towards declarative programming?

Mutable shared state is a frequent source of errors, especially when concurrency is involved.

  • Imperative: “I have a global variable x. Function A reads it. Function B changes it. Function C deletes it.” If B runs before A due to a thread issue (Race Condition), the program crashes.
  • Declarative: Data flows through pure functions. Input -> Function -> Output. The original input is untouched. This eliminates entire classes of concurrency bugs.

In imperative code, to understand what an algorithm does, you must “mentally execute it” (“okay, i starts at 0, enters the if, increments by 1…”). In declarative code, you read the intention. This can reduce visible complexity, although details still exist within the abstraction. :::

Declarative UI

Where this change has had the strongest impact is in User Interface (Frontend) development.

In approaches like WinForms or jQuery, it’s common to update the UI imperatively. WPF, on the other hand, already incorporated declarative mechanisms through XAML and data binding.

// Imperative (WinForms / Classic Android)
void OnButtonClick() {
    button.Text = "Loading..."; // We mutate the UI state manually
    label.Visible = true;
}
Copied!

Today (React, Flutter, MAUI, SwiftUI), the UI is declarative. The UI is a function of state: UI = f(State).

// Declarative (Concept like React/Blazor)
// We only define how the UI looks based on the state.
// The framework handles repainting whatever changes.
<Button Text="@(IsLoading ? "Loading..." : "Submit")" />
<Label Visible="@IsLoading" />
Copied!

Is imperative programming dead?

No. And it is important to understand this.

Declarative programming is an abstraction. Underneath, in the guts of the C# compiler or the JavaScript engine, someone had to write imperative code so that Where or Select work. The CPU still executes imperative instructions in assembly.

Furthermore, there are situations where extreme performance or total hardware control require an imperative approach (drivers, low-level video game engines).

However, for 95% of business logic and enterprise application architecture, the declarative approach is superior. It allows us to focus on the domain logic and forget about the control flow plumbing.