The Appeal of the Ada Language — Expressing Design Through Types and Powering Software That Runs for Decades

· · Ada, Programming Language, Strong Typing, SPARK, GNAT, Alire, High Integrity, Embedded, High Reliability

1. What to Understand First

Have you ever heard of a language called Ada?

Many people’s impression is “an old language,” “a military language,” or “I only ever heard the name in class.”

But Ada is very much a working language today.

In the world of software where a failure costs human lives — aircraft flight control, railway signaling systems, rockets, air traffic control, satellites, medical devices — Ada has been in continuous use for decades.

When trying to understand Ada, the key perspectives are these.

Ada is a language that pours everything into killing bugs before execution
Types are not containers for data but tools for expressing design intent
Separation of spec and implementation, contracts, and concurrency are built into the language
It never became fashionable, but its design philosophy lives on in modern languages

This article walks through the appeal of Ada: its history, syntax, strong typing, range constraints, packages, design by contract, tasking, SPARK, the development environment, and its weaknesses too.

The goal is for readers who normally write C#, C++, or Java to come away with the feel of “expressing design through types.”

The code fragments in this article are published on GitHub as a reference code collection, organized into files by chapter.

ada-language-appeal - komurasoft-blog-samples (GitHub)

2. What Is Ada? — The Name and the History

Ada is a general-purpose programming language born in the late 1970s under the leadership of the United States Department of Defense (DoD).

At the time, the DoD had a problem: every project used a different language, and software maintenance costs were ballooning.

So it held an international design competition to select a standard language that could also be used for embedded and real-time systems.

The winning design came from the team led by Jean Ichbiah.

The language’s name, Ada, comes from Ada Lovelace (Augusta Ada King, Countess of Lovelace), often called the world’s first programmer.

A rough timeline of Ada’s history looks like this.

1980 The first specification is established as MIL-STD-1815
1983 Ada 83 (ANSI standard)
1987 Becomes an ISO standard
1995 Ada 95 (introduces object orientation and protected objects)
2005 Ada 2005 (interfaces, expanded container library)
2012 Ada 2012 (design by contract introduced as a language feature)
2022 Ada 2022 (the latest standard)

Ada 95 was among the very first object-oriented languages to be ISO standardized.

And with Ada 2012, Design by Contract — preconditions, postconditions, and type invariants — was incorporated into the language specification itself.

Ada is not an “old language” — it is a language that has been continuously revised for more than 40 years.

3. Where Is Ada Used?

The classic domain where Ada continues to be used is systems that demand high integrity.

Flight control and avionics in commercial aircraft
Air traffic control systems
Railway signaling and safety systems
Rockets and satellites
Defense systems
Medical devices
Some core systems in finance and industry

These fields share common characteristics.

Bugs translate directly into loss of life or enormous financial losses
Certification and audits demand evidence of correctness
Once deployed, the system runs for decades
The cost of fixing things later is extreme

It is a world where “ship it now and fix it later” simply does not apply.

Ada’s language design exists precisely to answer these demands.

Bugs that can be caught at compile time are caught at compile time; what can only be caught at run time is caught by run-time checks; and going further, what can be proven mathematically is killed by proof — this philosophy runs through the entire language.

This philosophy is worth learning even for developers writing web or desktop business applications.

4. First, Hello, World

Let’s look at some Ada code.

with Ada.Text_IO;

procedure Hello is
begin
   Ada.Text_IO.Put_Line ("Hello, Ada!");
end Hello;

The first things you will notice are probably these.

with pulls in a library unit
The program body is a procedure
begin / end enclose the block
The name is repeated after end
Statements end with a semicolon

Writing the name again at the end, as in end Hello;, is quintessentially Ada.

Even when blocks nest deeply, “which end is this the end of” is visible at a glance.

The compiler also checks that the names match, so a mismatched block close is a compile error.

It looks like a small thing, but it perfectly illustrates Ada’s philosophy: the language supports humans exactly where humans tend to misread.

5. Syntax That Prioritizes Readability

Ada’s syntax was designed to prioritize readability over writability.

It stands on the premise that software is read far more often than it is written.

For example, loops and conditionals are written like this.

for I in 1 .. 5 loop
   Ada.Text_IO.Put_Line (Integer'Image (I * I));
end loop;

if Temperature > 80.0 then
   Start_Cooling;
elsif Temperature < 20.0 then
   Start_Heating;
else
   Keep_Current_State;
end if;

The case statement has a characteristically Ada flavor.

case Today is
   when Mon .. Fri =>
      Put_Line ("Weekday");
   when Sat | Sun =>
      Put_Line ("Weekend");
end case;

The points are as follows.

A case must cover every value or it is a compile error
There is no implicit fall-through as in C-family languages
Conditions can be grouped with ranges (Mon .. Fri) and alternatives (Sat | Sun)

Add a value to an enumeration, and every non-exhaustive case statement becomes a compile error.

The experience of “the compiler enumerates every place affected by a spec change” is one you cannot give up once you have tasted it.

Arguments can also use named association.

Draw_Rectangle (Left => 10, Top => 20, Width => 100, Height => 50);

It prevents argument mix-ups, and the calling code becomes its own documentation.

Assignment is := and comparison is =, so the C-family confusion of if (a = b) cannot happen at the syntax level.

6. Strong Typing — Turning Unit Mix-Ups into Compile Errors

Ada’s greatest appeal is its strong typing.

Many languages use the phrase “strongly typed,” but Ada’s version goes a level deeper.

In Ada, types declared under different names are different types, even when their structure is exactly the same.

type Meters  is new Float;
type Seconds is new Float;

Distance : Meters  := 100.0;
Time     : Seconds := 9.58;

Both of these are floating-point numbers underneath, but they cannot be mixed.

Distance := Time;            -- compile error
Distance := Distance + Time; -- compile error

Only when you convert deliberately do you write it out explicitly.

Speed : constant Float := Float (Distance) / Float (Time);

Why be this strict?

A surprising number of real-world software accidents are caused by “unit mix-ups.”

In a famous example, the Mars Climate Orbiter was lost in 1999 because imperial and metric units were mixed.

Ada’s answer is simple.

Make meters and feet different types
Make mixing them a compile error
Force conversions to be written explicitly

Not “be careful,” not “catch it in review,” not “catch it in tests” — make the build fail in the first place.

That is Ada’s fundamental stance.

7. Range Constraints — Stopping Invalid Values at the Data Type Level

In Ada, a type can carry a range of values.

subtype Percentage is Integer range 0 .. 100;

Progress : Percentage := 50;

Attempting to put an out-of-range value into a variable of type Percentage raises a Constraint_Error exception at run time.

Progress := 120;  -- Constraint_Error at run time

Violations that can be detected at compile time are detected at compile time.

The implicit assumptions — “this value should be 0 to 100,” “this value should be at least 1” — can be expressed in the type, not in a comment.

In fact, Ada’s standard library predefines commonly used constrained types.

Natural  = Integer range 0 .. Integer'Last
Positive = Integer range 1 .. Integer'Last

Furthermore, since Ada 2012 you can attach arbitrary conditions as predicates.

subtype Even is Integer
  with Dynamic_Predicate => Even mod 2 = 0;

Types aimed at hardware control, such as fixed-point types, are also built into the language.

type Temperature is delta 0.1 range -50.0 .. 150.0;

In most languages, checking for invalid values tends to look like this.

Validation via if statements at the top of functions
Missed checks left to code review
Unclear which functions receive already-validated values

In Ada you can say: “the moment a value is of this type, the range is already guaranteed.”

By shifting the responsibility for validation into the data type, function logic can concentrate on its real job.

8. Arrays and Indices — Bounds Checking and Enumeration Indices

Ada arrays let you freely choose the type of the index.

type Day is (Mon, Tue, Wed, Thu, Fri, Sat, Sun);

type Hours_Array is array (Day) of Natural;

Work_Hours : Hours_Array := (Mon .. Fri => 8, others => 0);

This is an array indexed by the enumeration type Day.

You access it as Work_Hours (Wed), with no need to remember “what does this numeric index mean.”

Loops can also follow the index type.

for D in Work_Hours'Range loop
   Put_Line (Day'Image (D) & ":" & Natural'Image (Work_Hours (D)));
end loop;

Attributes such as 'Range, 'First, 'Last, and 'Length give you the array’s bounds information at any time.

Because bounds are never hard-coded, resizing an array does not ripple into the loops.

And crucially, array accesses are always bounds-checked.

Buffer : String (1 .. 10);
Index  : Integer := 11;

Buffer (Index) := 'x';  -- Constraint_Error at run time

Buffer overruns in C/C++ have remained a leading cause of security vulnerabilities for decades.

In Ada, an out-of-bounds access is not undefined behavior but a defined exception.

Instead of silently corrupting memory and producing a mysterious crash somewhere else, the program stops loudly, immediately, at the point where the problem occurred.

Considering the investigation costs of long-running systems, this difference is enormous.

9. Packages — Separating Specification from Implementation

Ada’s module mechanism is the package.

A package is split into two files: the specification (spec) and the body.

counters.ads  spec: the interface exposed to the outside
counters.adb  body: the implementation details

The spec is written like this.

package Counters is

   type Counter is private;

   procedure Increment (C : in out Counter);
   function  Value     (C : Counter) return Natural;

private
   type Counter is record
      Count : Natural := 0;
   end record;

end Counters;

The body is written like this.

package body Counters is

   procedure Increment (C : in out Counter) is
   begin
      C.Count := C.Count + 1;
   end Increment;

   function Value (C : Counter) return Natural is
   begin
      return C.Count;
   end Value;

end Counters;

The points worth noticing are these.

Declaring a type private means clients cannot touch its internals
Reading only the spec (.ads) tells you everything about how to use it
Changing the body (.adb) requires minimal recompilation of clients as long as the spec stays the same

This resembles C/C++ header files, but instead of textual expansion like #include, consistency is checked as part of the language specification.

A mismatch between spec and body is a compile error.

Furthermore, every parameter must declare a mode: in, out, or in out.

procedure Increment (C : in out Counter);

Whether a parameter is read-only, write-only, or read-write is visible just from the signature.

Even without any knowledge of pointers or references, the direction of data flow can be read directly.

10. Records and Discriminants

Ada’s equivalent of a struct is the record.

type Point is record
   X : Float := 0.0;
   Y : Float := 0.0;
end record;

P : Point := (X => 1.0, Y => 2.0);

Fields can carry default values, and aggregates allow named initialization.

A characteristically Ada feature is the discriminant.

type Buffer (Size : Positive) is record
   Data   : String (1 .. Size);
   Length : Natural := 0;
end record;

Small : Buffer (Size => 16);
Large : Buffer (Size => 4096);

A discriminant is a parameter that determines the “shape” of a record.

Buffer (16) and Buffer (4096) are the same type, but the size of the internal array is fixed at declaration and never changes afterward.

Think of it as the language safely managing, as a type, what in C would be “a struct with a variable-length member plus a size field.”

The classic C bug entry point — inconsistency between the size field and the actual data — simply does not exist from the start.

11. Generics

Ada has had generics since its first standard in 1983.

That is far earlier than C++ templates (1990s) or Java generics (2004).

generic
   type Element is private;
procedure Swap (Left, Right : in out Element);

procedure Swap (Left, Right : in out Element) is
   Temp : constant Element := Left;
begin
   Left  := Right;
   Right := Temp;
end Swap;

The client instantiates it with concrete types.

procedure Swap_Integers is new Swap (Element => Integer);
procedure Swap_Floats   is new Swap (Element => Float);

The defining characteristic of Ada generics is that the required operations are stated explicitly.

generic
   type Element is private;
   with function "<" (Left, Right : Element) return Boolean is <>;
function Max (Left, Right : Element) return Element;

The spec says: “this generic function requires a comparison operator on the Element type.”

The problem that plagued C++ templates for years — errors only surfacing at instantiation — never arises in Ada.

The problem that C++20 concepts and Rust trait bounds set out to solve, Ada had an answer to 40 years ago.

12. Exception Handling

Ada has exception handling.

with Ada.Text_IO;
with Ada.Exceptions;

procedure Read_Config is
begin
   Load_File ("config.txt");
exception
   when Ada.Text_IO.Name_Error =>
      Ada.Text_IO.Put_Line ("Configuration file not found");
   when E : others =>
      Ada.Text_IO.Put_Line (Ada.Exceptions.Exception_Information (E));
      raise;
end Read_Config;

You write an exception part at the end of a block and list handlers per kind of exception.

The language-defined exceptions notably include the following.

Constraint_Error  range constraint violations, array bounds violations, division by zero, etc.
Program_Error     violations of language rules (e.g. reaching code that must not be reached)
Storage_Error     out of memory
Tasking_Error     failures in inter-task communication

What deserves attention is that violations of range constraints and bounds checks are all integrated into this exception mechanism.

When a “constraint written into the type” is broken, you get a Constraint_Error.

In other words, the range constraints we saw in chapter 7 function as automatically generated run-time assertions.

There is no need to scatter your own if-statement checking code everywhere.

13. Design by Contract — Writing Pre/Post Conditions as a Language Feature

The headline feature of Ada 2012 is language support for Design by Contract.

You can write preconditions (Pre) and postconditions (Post) directly on subprograms.

package Stacks is

   type Stack is private;

   function Is_Full  (S : Stack) return Boolean;
   function Is_Empty (S : Stack) return Boolean;
   function Count    (S : Stack) return Natural;

   procedure Push (S : in out Stack; Item : Integer)
     with Pre  => not Is_Full (S),
          Post => Count (S) = Count (S)'Old + 1;

   procedure Pop (S : in out Stack; Item : out Integer)
     with Pre  => not Is_Empty (S),
          Post => Count (S) = Count (S)'Old - 1;

private
   -- Implementation details
end Stacks;

Pre is “the promise the caller must keep”; Post is “the promise the implementation guarantees.”

The 'Old attribute lets you refer to the value before the call.

These contracts can be enabled as run-time checks via a compile option (-gnata in GNAT).

When a contract is violated, an Assertion_Error exception is raised, and which side broke the promise becomes clear.

Pre violation  -> a bug in the caller
Post violation -> a bug in the implementation

How is this different from writing “this function must not be called on an empty stack” in a documentation comment?

A comment can drift from the implementation and nobody notices
A contract has its syntax and types checked by the compiler
A contract can be verified automatically at run time
A contract becomes the input to static proof via SPARK (chapter 16)

The specification exists inside the code, in a verifiable form.

That is the world of Ada 2012 and later.

With type invariants (Type_Invariant), you can also write constraints of the form “values of this type always satisfy this property.”

14. Tasks — Concurrency Built into the Language

Ada’s other great appeal is that concurrency is part of the language specification.

Where C/C++ rely on OS APIs or libraries (pthread, std::thread) for threads, Ada built tasks into the language as of 1983.

with Ada.Text_IO;

procedure Task_Demo is

   task Worker;

   task body Worker is
   begin
      for I in 1 .. 3 loop
         Ada.Text_IO.Put_Line ("worker:" & Integer'Image (I));
         delay 0.5;
      end loop;
   end Worker;

begin
   for I in 1 .. 3 loop
      Ada.Text_IO.Put_Line ("main  :" & Integer'Image (I));
      delay 0.5;
   end loop;
end Task_Demo;

Declare a task, and concurrent execution begins the moment the enclosing block starts.

And crucially, the block does not finish until all the tasks inside it have finished.

The whole class of bugs along the lines of “forgot to join the thread, so strange things happen at process exit” is structurally impossible.

For synchronization between tasks, the language provides the rendezvous.

task Logger is
   entry Write (Message : String);
end Logger;

task body Logger is
begin
   loop
      select
         accept Write (Message : String) do
            Ada.Text_IO.Put_Line (Message);
         end Write;
      or
         terminate;
      end select;
   end loop;
end Logger;

The client writes Logger.Write ("hello"); — the same shape as a procedure call.

Message-passing communication between tasks can be written without any knowledge of locks.

For real-time systems, scheduling policy and priority control are standardized — and so is the Ravenscar profile, which restricts the tasking features for the sake of verifiability.

15. Protected Objects — Writing Mutual Exclusion as a Type

For mutual exclusion over shared data, you use protected objects, introduced in Ada 95.

protected Shared_Counter is
   procedure Increment;
   function  Value return Natural;
private
   Count : Natural := 0;
end Shared_Counter;

protected body Shared_Counter is

   procedure Increment is
   begin
      Count := Count + 1;
   end Increment;

   function Value return Natural is
   begin
      return Count;
   end Value;

end Shared_Counter;

The data of a protected object can only be accessed through the operations you define.

And the mutual exclusion is guaranteed by the language.

procedure  read/write allowed, executes exclusively
function   read-only, allows simultaneous execution by multiple tasks
entry      can make callers wait until a condition (barrier) is satisfied

In most languages, mutual exclusion tends to rest on discipline.

Take this mutex whenever touching this data
Do not forget to release the lock
Respect the lock ordering

With Ada’s protected objects, “code that forgot to take the lock” cannot be written in the first place.

The data and the mutual exclusion that guards it are declared as a single type.

Using entry barrier conditions, condition synchronization like “wait until data arrives in the queue” can also be written without manually managing flags or condition variables.

16. SPARK — The Road to Formal Verification

The Ada world has a powerful companion: SPARK.

SPARK is a subset of Ada designed so that properties of a program can be proven mathematically.

procedure Increment (X : in out Integer)
  with SPARK_Mode,
       Pre  => X < Integer'Last,
       Post => X = X'Old + 1;

The SPARK tooling (GNATprove) proves things like the following about this code — without executing it.

No overflow can occur
No range constraint violation can occur
No division by zero can occur
No uninitialized variable is read
Pre and Post are consistent

The difference from testing is decisive.

Testing  confirms correct behavior for the inputs you chose
Proof    shows the property holds for all inputs

The contracts (Pre/Post) we saw in chapter 13 become, in SPARK, the direct subject of proof.

Contracts you wrote as run-time checks can later be promoted to “proven.”

SPARK built its track record in the aerospace and defense world, but in recent years industrial use has been spreading — NVIDIA, for instance, adopted it for firmware security.

The conventional wisdom that “formal methods are too academic for real work” is being quietly overturned, again and again, by the Ada/SPARK ecosystem.

17. Interoperating with C and C++

Ada is not an isolated language.

Interoperation with C is standardized in the language specification (Annex B).

For example, to call the Windows API Sleep from Ada, you write this.

with Interfaces.C;

procedure Sleep_Demo is

   procedure Sleep (Milliseconds : Interfaces.C.unsigned)
     with Import,
          Convention    => Stdcall,
          External_Name => "Sleep";

begin
   Sleep (1000);
end Sleep_Demo;

The points are as follows.

Import           pulls in an external implementation
Convention       specifies the calling convention (C, Stdcall, etc.)
External_Name    specifies the symbol name at link time
Interfaces.C     provides types corresponding to C types (int, unsigned, char*, etc.)

The reverse direction is possible too.

With Export, a procedure written in Ada can be exposed as a function callable from C.

That enables a staged approach like this.

Use existing C libraries from Ada
Write only the system's core in Ada/SPARK, leaving the periphery in C/C++
Build Ada code into a DLL and call it from other languages

Ada is not a language whose only path is “rewrite everything” — it can coexist with existing assets while raising reliability starting from the parts that matter most.

18. The Development Environment — GNAT and Alire (Works on Windows Too)

You might think “trying Ada must require expensive tools.”

Today, a full-fledged development environment is available for free.

GNAT        the Ada compiler included in GCC (free)
Alire       Ada's package manager and build tool
GNAT Studio the IDE from AdaCore
VS Code     completion and go-to-definition via the Ada Language Server extension

The arrival of Alire in particular (the command is alr) made getting started with Ada dramatically easier.

The experience is close to Rust’s cargo.

alr init --bin hello_ada
cd hello_ada
alr build
alr run

alr init creates the project, alr build builds it, and alr run runs it.

Alire even fetches the toolchain (GNAT itself), so you do not need to install the compiler manually.

It works on Windows, Linux, and macOS alike.

If you develop on Windows, this is the shortest path.

1. Get the Windows installer from the official Alire site
2. Create a skeleton with alr init --bin
3. Install the Ada extension (by AdaCore) in VS Code
4. Build and run with alr build

Libraries can be added with alr with <library name>.

The era of “giving up at environment setup” is over.

19. Ada’s Weaknesses and Caveats

We have covered the appeal so far, but Ada has weaknesses too.

Let’s lay them out fairly.

The ecosystem is small
  Few options for web frameworks, GUIs, cloud SDKs, and so on
  Alire's package count is orders of magnitude below mainstream languages

Talent and information are scarce
  Information in Japanese is especially scarce
  Adopting it for team development requires budgeting for training

The syntax can feel verbose
  Type declarations and spec/body separation are heavy for small scripts
  Not suited to "just get something running"

The job market is limited
  Skewed toward aerospace, defense, rail, and similar fields

History also teaches us that “use Ada and you’re safe” is not how it works.

The explosion of the first Ariane 5 rocket in 1996 had software written in Ada as one of its causes.

Code written for Ariane 4 was reused on Ariane 5, which has different flight characteristics; an unexpectedly large value triggered a Constraint_Error during conversion, it was not handled appropriately, and the system shut down.

What this accident shows is the following.

The language's run-time checks did detect the problem (it did not fail silently)
But the operating assumptions changed and were never re-verified
The design for what happens after an exception (fail-safe) was inadequate

Neither type systems nor contracts can substitute for the process of revisiting your assumptions.

The language is part of safety engineering, not the whole of it.

I think that is the most honest caveat to carry while learning Ada.

20. Long-Lived Software and Ada — From a Maintenance Perspective

On this site, we frequently deal with maintaining and extending the life of existing Windows assets.

From that perspective, Ada has another kind of appeal.

It is not unusual for systems written in Ada to keep running on a timescale of decades.

And Ada’s language design itself assumes long-term maintenance.

Separation of spec (.ads) and implementation (.adb)
  -> A maintainer 20 years later can grasp the interface by reading only the spec

Strong types and range constraints
  -> Implicit assumptions stay in the code, not in oral tradition or comments

Contracts (Pre/Post)
  -> "This function's promises" remain in a verifiable form

Exhaustiveness checking of case
  -> The compiler enumerates the places affected by a spec change

Backward compatibility valued even across standard revisions
  -> Much Ada 83 code still compiles with modern compilers

All of these can be imported directly as design guidelines when doing long-term maintenance in C# or C++ as well.

Define meaningful types (ID types, unit-bearing types) instead of raw int
Design types whose invalid values cannot be constructed (validation in constructors)
Consciously separate public interfaces from implementations
Express preconditions and postconditions via assertions and tests
Write enum switches exhaustively and treat the warnings as errors

Even if you never get to use Ada at work, learning Ada’s design philosophy is well worth it.

As teaching material for the feel of “expressing design through types,” Ada remains first-rate today.

21. Summary

We have walked through the appeal of Ada.

Let’s review the key points.

Ada is a working language, in continuous use for over 40 years in high-reliability systems
The name comes from Ada Lovelace; the latest standard is Ada 2022
Identically structured types with different names are different types; mixing units is a compile error
Range constraints stop invalid values at the type level
Arrays are bounds-checked; buffer overruns are not undefined behavior
Packages separate spec from implementation; parameter modes make data flow explicit
Generics state the required operations in the spec, so usage errors are clear
Ada 2012 contracts (Pre/Post) keep the specification in the code in verifiable form
Tasks and protected objects make concurrency safe to write as a language feature
With SPARK, contracts can be promoted from run-time checks to mathematical proof
With GNAT and Alire, you can try it immediately, for free, even on Windows
Its weaknesses are the small ecosystem and the scarcity of talent
The language's safety machinery is no substitute for the process of revisiting assumptions

In terms of popularity, Ada is a language that never became mainstream.

But much of what modern languages introduce as “new features” — null safety, exhaustiveness checking, contracts, a rigor approaching ownership — Ada has had for decades.

The essence of Ada can be summed up in one line.

Bugs are not something you find — they are something types and contracts make impossible to write.

Some weekend, create a project with Alire and write a small program while the compiler scolds you.

The moment you realize that every one of those compile errors is “a bug caught before it became a production incident,” the appeal of Ada will click into place.

References

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog