Natural ADABAS migration: from MU/PE fields and descriptors to SQL, Java and C#
Most Natural ADABAS migrations go wrong in the data layer, not in the code. Natural statements look easy to transliterate, but they sit on an inverted-list database whose rules differ from SQL: repeating groups inside a record, null suppression that hides rows from an index, and record holds that act as the transaction model.
This guide is for architects, modernization leads and Natural developers moving an Adabas and Natural estate to a relational database with Java or C# on top. It explains the ADABAS internals that matter, the Natural statements that depend on them, and the relational mapping for each, then works one example through Natural, PostgreSQL DDL, Spring Data JPA and EF Core. For the wider decision framework, start with our legacy software modernization guide; for delivery, see our Natural and ADABAS to Java migration service.
Key takeaways
- ADABAS is not orphaned. Silver Lake, which owns Software AG, runs Adabas & Natural as a standalone business that still ships releases. Migrate for cost, skills or architecture, not fear.
- MU fields and PE groups become child tables keyed by parent and occurrence number; MU inside PE needs a third level.
- Descriptors become indexes, but null suppression, superdescriptor rules and EBCDIC collation change which rows come back.
- Keep the original ISN as a column for reconciliation and ISN-ordered logic. Where ISN-order is irrelevant and a designated primary key exist, it can be used instead.
- Record hold plus END TRANSACTION maps to SELECT ... FOR UPDATE plus commit, but chunking and FIND snapshot semantics need deliberate design.
- Prove the result with parallel runs and record-level comparison, not code review.
What is Natural ADABAS?
Natural ADABAS is the pairing of Adabas, Software AG's inverted-list database, with Natural, the fourth-generation language built to read and update it. Natural programs access Adabas files through data definition modules (DDMs) and navigate by descriptors, ISNs and logical sequence rather than SQL joins. The pair runs core government, insurance and banking systems.
The coupling is tight. Natural's loop structure, hold logic and array syntax mirror the database's physical design, which is why a Natural migration is always a data migration too.
Who owns Adabas and Natural in 2026
Silver Lake acquired a controlling stake in Software AG in 2023, and the group then sold most of its portfolio, including StreamSets and webMethods to IBM (Wikipedia summary of Software AG).
On January 7, 2025, Software AG announced that its holding company, Software GmbH, had closed the sales of Alfabet and Cumulocity, after selling webMethods, StreamSets and TrendMiner in July 2024. It said it would run Adabas & Natural and ARIS as standalone businesses, each with its own management team, under Silver Lake ownership. As of October 2026 we found no announcement changing this structure, but recheck before it goes into a board paper.
The product line is active. The Adabas & Natural release information for October 2025 lists Natural for z/OS 9.2.4, Natural for Linux 9.3.3, Adabas for Linux 7.4 and Predict 8.6.1, plus Co-Innovation items such as Natural for Visual Studio Code planned for October 2026. It also notes licensing changes, including a separate license for Natural Security on z/OS.
So "the vendor is going away" is not a credible driver. Licensing cost, MIPS, the shrinking Natural talent pool and the need for relational analytics are.
ADABAS database internals that drive migration decisions
These internals show up as behavior differences after a naive conversion. The authoritative reference is Software AG's Adabas design concepts documentation.
Associator, Data Storage and the address converter
Data Storage holds compressed records in blocks. The Associator holds the access structures: inverted lists, the address converter and each file's Field Definition Table (FDT). Work holds recovery data and intermediate ISN lists.
A descriptor's inverted list is organized by value: each entry holds the value, a record count and the matching ISNs. Searches resolve entirely in the Associator, and Data Storage is read only for records the program needs. The address converter maps each ISN to its block, so records can move without touching any index.
ISNs: identity without a primary key
Every record carries an internal sequence number (ISN) that never changes. New records normally get the highest ISN plus one, and deleted ISNs are reused only if the DBA configures it.
Applications use ISNs through GET, READ BY ISN, and stored ISNs used as pointers between files. The last is the dangerous one: an ISN of file B stored in file A is a foreign key nobody declared, and your migration must find it.
Descriptors: super, sub, hyper, phonetic and collation
A descriptor (DE) is an indexed field; UQ makes it unique. Derived keys include:
- Subdescriptor: part of one field.
- Superdescriptor: all or parts of 2 to 20 fields, the closest thing to a composite index.
- Hyperdescriptor: keys generated by programmatic user code, often in Assembler.
- Phonetic descriptor: sound-alike matching on names.
- Collation descriptor: a custom sort order supplied by a user exit.
Sub and superdescriptors map cleanly to expression and composite indexes. Phonetic and hyperdescriptors must be re-implemented and proved to produce the same key values.
Null suppression and empty values
Adabas compresses records by removing trailing blanks and leading zeros. A field defined with NU is null suppressed: records with an empty value do not appear in that descriptor's inverted list, so FIND and READ LOGICAL by it never return them.
A superdescriptor value is also not generated when a null-suppressed component is empty. A record with a blank NAME vanishes from a DEPT-NAME superdescriptor even if DEPT is filled. Relational indexes do not do this, so the SQL must filter explicitly.
MU fields, PE groups and MU within PE
A multiple-value (MU) field holds a list of values, such as languages spoken. A periodic (PE) group repeats a set of fields, such as salary history. An MU field can sit inside a PE group, giving a two-dimensional array, but PE groups cannot nest. The default limit is 191 occurrences, extendable to roughly 65,534.
Occurrence numbers carry meaning. Many applications treat PE occurrence 1 as "current" and shift history down on each update, a business rule encoded in physical layout.
FDT, DDMs and Predict
The FDT defines each field's two-character short name, level, length, format and options. Natural programs use a DDM that gives fields long names. Predict, the data dictionary, typically documents files, unenforced relationships and cross-references showing which programs touch which fields.
The FDT for the example file used throughout this post:
| Level | Short name | DDM long name | Length and format | Options |
|---|---|---|---|---|
| 1 | AA | PERSONNEL-ID | 8 A | DE, UQ |
| 1 | AB | NAME | 20 A | DE, NU |
| 1 | AD | CITY | 20 A | DE, NU |
| 1 | AE | DEPT | 6 A | DE, NU |
| 1 | AF | LANG | 3 A | DE, NU, MU |
| 1 | AG | INCOME | PE group | PE |
| 2 | AH | CURR-CODE | 3 A | NU |
| 2 | AI | SALARY | 5 P | NU |
| 2 | AJ | BONUS | 5 P | NU, MU |
| Super | S1 | DEPT-NAME | AE(1-6) + AB(1-20) | derived |
| Phonetic | PH | PH-NAME | from AB | derived |
The FDT says 5 P (five bytes); the DDM shows P9 (nine digits). Confusing the two is a common cause of wrong NUMERIC precision in generated DDL.
The Natural programming language from a migration point of view

The Natural programming language has a small core and many database-aware statements. These parts shape the target design.
Reporting mode vs structured mode
In reporting mode, loops can close with LOOP or END, and database fields need no declaration. In structured mode, every loop has an explicit closer (END-READ, END-FIND) and all data is defined in DEFINE DATA or an external data area. Software AG recommends structured mode exclusively.
Old estates mix both. Reporting-mode code is harder to translate because loop scope and types must be inferred, so normalizing it to structured mode first is a useful, verifiable step.
Database statements and what they really do
From Software AG's guide to accessing Adabas from Natural, the migration-relevant behavior:
- READ PHYSICAL returns records in storage order, effectively arbitrary; READ BY ISN uses ISN order.
- READ LOGICAL BY descriptor follows descriptor-value order between STARTING FROM and ENDING AT. Reading by an MU descriptor returns a record once per qualifying value.
- FIND ... WITH builds an ISN list from descriptor criteria; WHERE filters afterwards on any field.
- HISTOGRAM reads only the inverted list: distinct values and counts.
- GET reads one record by ISN.
- STORE, UPDATE and DELETE change data; UPDATE writes only the fields in the view.
Record hold, END TRANSACTION and BACKOUT
Natural puts records in hold automatically when a loop contains UPDATE or DELETE, and they stay locked until END TRANSACTION or BACKOUT TRANSACTION. Other users wait for held records unless told not to, and the Adabas transaction time limit backs out work held too long. END TRANSACTION can also store restart data. Batch programs that commit every N records rely on all of this to limit lock duration and restart cost.
AT BREAK, maps and reports
AT BREAK OF field fires when a control field changes in a sorted loop, with system functions such as OLD, COUNT and SUM. It fires once more at end of data, which hand translations often forget.
Maps are Natural's screen definitions. Their field validation, processing rules and PF-key handling belong in the UI or API layer after migration.
Subprograms, subroutines and data areas
- CALLNAT invokes a subprogram. Parameters are passed by address, so changes the subprogram makes are visible to the caller, and the subprogram cannot see the caller's global data area.
- PERFORM runs an internal or external subroutine that shares the caller's data.
- FETCH transfers control to another program without returning; FETCH RETURN comes back.
- LDA, GDA, PDA: a local data area is private to a module, a global data area is shared across a call chain, and a parameter data area defines a subprogram's interface.
In Java or C#, a PDA becomes a method signature or parameter object, an LDA becomes local fields, and a GDA becomes an explicit context object. GDAs used as hidden channels between modules are the design debt to surface early.
Mapping ADABAS to SQL: concept by concept
This table is the core of any Adabas to SQL design, whether the target is PostgreSQL, SQL Server, Oracle or Db2. For VSAM, IMS and Db2 sources, see our companion piece on mainframe data migration to relational databases.
| ADABAS / Natural concept | Relational target | Java (Spring/JPA) | C# (EF Core) | Gotcha |
|---|---|---|---|---|
| File | Table | @Entity | Entity class | Natural views select subsets of fields |
| ISN | Surrogate key plus legacy_isn column; customer-specified primary key possible where ISN is irrelevant for access | @Id plus field | Key plus property | Stored ISNs in other files are hidden foreign keys |
| MU field | Child table (parent, seq, value) | @OneToMany + @OrderBy | Owned or child collection | Occurrence order is data |
| PE group | Child table (parent, seq, fields) | @OneToMany | Child collection | Gaps and "occurrence 1 = current" rules |
| MU within PE | Grandchild table | Nested collection | Nested collection | Two-dimensional index BONUS(i,j) |
| Descriptor / UQ | Index / unique index | @Index or DDL | HasIndex().IsUnique() | NU means rows with empty values are excluded |
| Superdescriptor | Composite or partial index | DDL | DDL | Not generated if an NU component is empty |
| Phonetic / hyperdescriptor | Function index or computed column | Re-implemented logic | Re-implemented logic | Must reproduce key values exactly |
| READ LOGICAL | ORDER BY with keyset pagination | Keyset Window or JPQL | OrderBy + Where seek | Collation differs from EBCDIC |
| FIND WITH | SELECT ... WHERE | Repository query | LINQ query | Snapshot ISN list vs live query |
| HISTOGRAM | GROUP BY value with COUNT(*) | Projection query | GroupBy | Exclude NULLs to mimic NU |
| Hold + END TRANSACTION | SELECT ... FOR UPDATE + COMMIT | @Lock(PESSIMISTIC_WRITE) | Raw SQL FOR UPDATE | Lock duration and chunking |
MU and PE fields to child tables
Each MU field and each PE group becomes a child table with the parent key, an occurrence number and the value columns. An MU inside a PE becomes a grandchild keyed by the PE row and its own occurrence number.
Keep the occurrence number. Natural code references SALARY(1) and LANG(1:3) by position, and only ordering by occurrence reproduces that. We recommend loading every occurrence up to the C* count, empty or not, so positions are preserved.
Search semantics change too. FIND EMP WITH LANG = 'FRE' matches if any occurrence matches, an EXISTS subquery in SQL. A FIND on two fields of the same PE group generally does not require both to match in the same occurrence, so a single join condition changes the result set. Use two independent EXISTS clauses unless the program clearly intends occurrence correlation.
Descriptors to indexes
Descriptors become B-tree indexes, UQ descriptors become unique constraints, and superdescriptors become composite or expression indexes.
For NU descriptors, apply one rule everywhere: load empty values as NULL, filter IS NOT NULL in translated reads, and use partial indexes such as WHERE city IS NOT NULL. Non-descriptor numerics are simpler as NOT NULL DEFAULT 0, because Natural treats an empty numeric as zero.
ISN to surrogate keys
Use a generated surrogate key and carry the original ISN in a unique legacy_isn column. Reconciliation becomes trivial, and READ BY ISN or order-sensitive FIND loops become ORDER BY legacy_isn. If code still depends on ISN order, new rows need a sequence starting above the highest migrated ISN.
READ LOGICAL to ordered queries with keyset pagination
READ LOGICAL is a cursor walking an index from a start value. The relational equivalent is keyset (seek) pagination, not OFFSET. Microsoft's EF Core documentation explains why keyset pagination beats offset paging: offset rescans skipped rows and can skip or repeat rows under concurrent updates. Add the primary key as a tiebreaker so ordering is unique. Spring Data offers the same pattern through keyset-based Window scrolling.
Order itself also shifts. Without ORDER BY, PostgreSQL returns rows in whatever order it finds fastest. On the mainframe, Adabas compares descriptor values as EBCDIC bytes, so letters sort before digits; COLLATE "C" gives ASCII order, where digits come first. Reports, STARTING FROM/ENDING AT ranges and control breaks can all move. Our analysis of EBCDIC to ASCII sort order mismatches covers the fixes.
Record holding to transactions
Natural's hold maps to SELECT ... FOR UPDATE, with NOWAIT where the program used Natural's no-wait option. END TRANSACTION becomes commit, BACKOUT TRANSACTION becomes rollback, and the Adabas transaction time limit corresponds roughly to PostgreSQL's lock_timeout and idle_in_transaction_session_timeout.
Two differences need design work. A classic Adabas read without hold can see another user's uncommitted update; PostgreSQL never returns uncommitted data. And a FIND builds its ISN list once, while chunked SQL re-evaluates the predicate per chunk.
Before and after: Natural to Java and C#
The example gives one department a 3.5% raise on current salary (PE occurrence 1), commits every 50 records, then prints department totals with a control break.
The Natural original
DEFINE DATA LOCAL
1 EMP VIEW OF EMPLOYEES
2 PERSONNEL-ID
2 NAME
2 DEPT
2 C*INCOME
2 INCOME (1:5)
3 CURR-CODE
3 SALARY
1 #DEPT (A6)
1 #FACTOR (N1.4) INIT <1.0350>
1 #UPDATED (P7)
1 #SINCE-ET (P3)
1 #CUR-SAL (P9)
END-DEFINE
*
INPUT 'Department:' #DEPT
*
FIND EMP WITH DEPT = #DEPT
IF C*INCOME = 0
ESCAPE TOP
END-IF
COMPUTE ROUNDED SALARY (1) = SALARY (1) * #FACTOR
UPDATE
ADD 1 TO #UPDATED
ADD 1 TO #SINCE-ET
IF #SINCE-ET >= 50
END TRANSACTION
RESET #SINCE-ET
END-IF
END-FIND
END TRANSACTION
WRITE 'Updated:' #UPDATED
*
READ EMP BY DEPT STARTING FROM 'TECH' ENDING AT 'TECH99'
#CUR-SAL := SALARY (1)
AT BREAK OF DEPT
WRITE OLD(DEPT) 5X COUNT(#CUR-SAL) 5X SUM(#CUR-SAL)
END-BREAK
END-READ
END
Target DDL for PostgreSQL (derived from the DDM, not the Natural VIEW)
CREATE TABLE employee (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
legacy_isn bigint NOT NULL UNIQUE, -- original Adabas ISN
personnel_id varchar(8) COLLATE "C" NOT NULL UNIQUE, -- AA: DE,UQ
name varchar(20) COLLATE "C", -- AB: DE,NU (empty -> NULL)
city varchar(20) COLLATE "C", -- AD: DE,NU
dept varchar(6) COLLATE "C" -- AE: DE,NU
);
CREATE INDEX ix_employee_city ON employee (city) WHERE city IS NOT NULL;
CREATE INDEX ix_employee_dept_name ON employee (dept, name) -- superdescriptor S1
WHERE dept IS NOT NULL AND name IS NOT NULL;
CREATE TABLE employee_lang ( -- MU field LANG
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
employee_id bigint NOT NULL REFERENCES employee(id) ON DELETE CASCADE,
seq smallint NOT NULL, -- occurrence number
lang char(3) COLLATE "C" NOT NULL,
UNIQUE (employee_id, seq)
);
CREATE INDEX ix_employee_lang ON employee_lang (lang, employee_id);
CREATE TABLE employee_income ( -- PE group INCOME
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
employee_id bigint NOT NULL REFERENCES employee(id) ON DELETE CASCADE,
seq smallint NOT NULL, -- 1 = current salary
curr_code char(3),
salary numeric(9,0) NOT NULL DEFAULT 0, -- FDT 5 P = 9 digits
UNIQUE (employee_id, seq)
);
CREATE TABLE employee_income_bonus ( -- MU BONUS inside PE
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
income_id bigint NOT NULL REFERENCES employee_income(id) ON DELETE CASCADE,
seq smallint NOT NULL,
bonus numeric(9,0) NOT NULL DEFAULT 0,
UNIQUE (income_id, seq)
);
Java with Spring Data JPA
@Entity
@Table(name = "employee")
public class Employee {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@Column(name = "legacy_isn") private Long legacyIsn;
@Column(name = "personnel_id") private String personnelId;
private String name;
private String dept;
@OneToMany(mappedBy = "employee", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("seq ASC")
private List<IncomeOccurrence> incomes = new ArrayList<>();
public Optional<IncomeOccurrence> occurrence(int seq) {
return incomes.stream().filter(i -> i.getSeq() == seq).findFirst();
}
public String getPersonnelId() { return personnelId; }
}
@Entity
@Table(name = "employee_income")
public class IncomeOccurrence {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "employee_id") private Employee employee;
private short seq; // PE occurrence, 1-based
private BigDecimal salary; // numeric(9,0)
// getters and setters omitted
}
public record DeptTotal(String dept, Long employees, BigDecimal payroll) {
public DeptTotal { payroll = payroll == null ? BigDecimal.ZERO : payroll; } // SUM of none = 0
}
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select e from Employee e where e.dept = :dept and e.personnelId > :after "
+ "order by e.personnelId")
List<Employee> lockNextChunk(@Param("dept") String dept,
@Param("after") String after, Pageable page);
@Query("select new com.example.payroll.DeptTotal(e.dept, count(e), sum(i.salary)) "
+ "from Employee e left join e.incomes i on i.seq = 1 "
+ "where e.dept between :from and :to group by e.dept order by e.dept")
List<DeptTotal> totalsByDept(@Param("from") String from, @Param("to") String to);
}
@Service
public class SalaryRaiseService {
private static final int COMMIT_EVERY = 50; // the Natural ET interval
private final EmployeeRepository repo;
private final TransactionTemplate tx;
public SalaryRaiseService(EmployeeRepository repo, PlatformTransactionManager tm) {
this.repo = repo;
this.tx = new TransactionTemplate(tm);
}
public int raiseDepartment(String dept, BigDecimal factor) {
AtomicInteger updated = new AtomicInteger();
String after = "";
while (true) {
final String from = after;
List<String> keys = tx.execute(status -> { // one END TRANSACTION per chunk
List<Employee> chunk = repo.lockNextChunk(dept, from, PageRequest.of(0, COMMIT_EVERY));
for (Employee e : chunk) {
e.occurrence(1).ifPresent(inc -> { // SALARY (1)
inc.setSalary(inc.getSalary().multiply(factor).setScale(0, RoundingMode.HALF_UP));
updated.incrementAndGet();
});
}
return chunk.stream().map(Employee::getPersonnelId).toList();
});
if (keys.isEmpty()) return updated.get();
after = keys.get(keys.size() - 1);
}
}
}
C# with EF Core
// Columns mapped with UseSnakeCaseNamingConvention() (EFCore.NamingConventions)
[Table("employee")]
public sealed class Employee
{
public long Id { get; set; }
public long LegacyIsn { get; set; }
public string PersonnelId { get; set; } = "";
public string? Name { get; set; }
public string? Dept { get; set; }
public List<IncomeOccurrence> Incomes { get; } = new();
}
[Table("employee_income")]
public sealed class IncomeOccurrence
{
public long Id { get; set; }
public long EmployeeId { get; set; }
public short Seq { get; set; } // PE occurrence, 1-based
public decimal Salary { get; set; } // HasPrecision(9, 0)
}
public async Task<int> RaiseDepartmentAsync(string dept, decimal factor, CancellationToken ct)
{
var updated = 0;
var after = "";
while (true)
{
await using var tx = await _db.Database.BeginTransactionAsync(ct);
// The Natural hold made explicit: lock the next 50 rows in key order.
var keys = await _db.Database.SqlQuery<string>($"""
SELECT personnel_id AS "Value" FROM employee
WHERE dept = {dept} AND personnel_id > {after}
ORDER BY personnel_id LIMIT 50 FOR UPDATE
""").ToListAsync(ct);
if (keys.Count == 0) return updated;
var batch = await _db.Employees
.Where(e => keys.Contains(e.PersonnelId))
.Include(e => e.Incomes.Where(i => i.Seq == 1))
.ToListAsync(ct);
foreach (var inc in batch.SelectMany(e => e.Incomes))
{
inc.Salary = Math.Round(inc.Salary * factor, 0, MidpointRounding.AwayFromZero);
updated++;
}
await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct); // END TRANSACTION
after = keys[^1];
}
}
Gotchas the side-by-side hides
- Rounding. Natural COMPUTE ROUNDED rounds half away from zero. Java's RoundingMode.HALF_UP matches; C# Math.Round defaults to banker's rounding, so MidpointRounding.AwayFromZero is mandatory.
- The final control break. GROUP BY handles the end-of-data break; a hand-written "previous department" loop usually drops the last group.
- Counting. COUNT(#CUR-SAL) counts every record read, including those with no income, hence count(e) with a left join.
- Collation. In EBCDIC, 'TECHA1' falls inside 'TECH' to 'TECH99'; with COLLATE "C", it falls outside.
- Snapshot vs live predicate. The FIND list is fixed at the start; the SQL re-queries each chunk. Harmless for a raise, risky for "process each item once" batches.
- Lock acquisition. Natural holds records one at a time; the SQL locks 50 at once, which can change deadlock patterns.
Natural ADABAS migration options compared

A fair comparison has to include staying put. Our mainframe migration strategy playbook covers the platform side; this table focuses on Natural and Adabas.
| Option | What changes | Good fit when | Main risk |
|---|---|---|---|
| Retain on z/OS | Nothing; optimize and document | Stable system, skilled team | Skills attrition, license and MIPS cost |
| Replatform to Adabas & Natural on Linux | Platform only | Goal is leaving the mainframe, not Natural | Still a Natural shop; EBCDIC changes |
| Data first, keep Natural | Adabas to Db2 via Natural for Db2 | Relational data access is urgent | Two migrations instead of one |
| Translate Natural to Java or C# | Code and data together | Large estate, rules must be preserved | Translation fidelity, data semantics |
| Rewrite | New system from requirements | Business process is changing anyway | Lost undocumented rules |
| GenAI-assisted conversion | LLM output plus review | Small, well-tested modules | Non-determinism, subtle errors |
There is no shame in the first two rows. If the system works and the team is stable, read our argument for when not to modernize first. Rehosting on Linux is vendor-supported and actively released. Broader rehost, refactor and rearchitect trade-offs are in the legacy modernization pillar guide.
A step-by-step migration plan
- Inventory everything. Natural libraries, DDMs, FDTs, Predict entries, descriptor exits, Entire Operations job networks and every interface. Our checklist of everything on the mainframe that is not COBOL applies to Natural shops too.
- Extract the real data model. Combine FDTs, Predict and code analysis to find stored ISNs, implicit foreign keys and occurrence-position rules. Legacy code analysis and business rules extraction finds what the dictionary does not say.
- Normalize to structured mode. Convert reporting-mode code, resolve GDA usage, and identify dynamic CALLNAT and FETCH targets.
- Design the relational schema. Apply the mapping table, fix NU and collation rules once, and keep legacy_isn.
- Rehearse the data load. Unload decompressed, convert EBCDIC and packed decimals, expand MU and PE groups, and reconcile counts per file, occurrence and descriptor value.
- Translate the code. Generate Java or C# services, data access and UI or API layers from Natural source and maps.
- Prove equivalence. Run both systems in parallel on the same inputs and compare records and reports, as described in our guide to data migration testing and functional equivalence.
- Cut over in waves. Move application areas with the files they own, using change data capture for shared files.
How deterministic translation handles Natural and ADABAS
A deterministic translator treats Natural like a compiler does. It parses both programming modes, resolves every view against the DDM and FDT, builds control-flow and data-flow graphs, and applies rewrite rules that are proved once and applied everywhere. Modernize Software's DMS program-transformation engine works this way and, the company reports, produces the same output from the same input on every run. See how deterministic software transformation works for the mechanics.
That analysis answers questions a line-by-line converter cannot. Which PE occurrences does each program touch? Is a stored ISN used as a pointer? Does a loop depend on ISN order or record-by-record holds? The answers decide whether a FIND becomes a simple query or an ISN-ordered, snapshot-preserving one.
The limits are real. Dynamic CALLNAT targets held in variables, source generated and executed with RUN, commands pushed onto the Natural stack, and Assembler hyperdescriptor exits cannot always be resolved statically. Modernize Software reports that such constructs are flagged for engineer review rather than guessed. The same engine targets .NET through the ADABAS to C# migration service, and the customer owns the generated code with no runtime royalties.
Illustrative scenario: a benefits system leaving Adabas
This is an illustrative composite, not a named customer engagement. A public agency runs benefit eligibility on Natural and Adabas under z/OS: a few thousand Natural objects, a few dozen Adabas files, and Entire Operations batch cycles. The target is PostgreSQL and Java on Linux with eligibility rules unchanged.
The inventory finds three things the documentation missed. A claimant file stores the ISN of the current address record in another file. A payment-history PE group treats occurrence 1 as the latest payment and shifts history down on each update. And one report sorts reference numbers mixing letters and digits, so the EBCDIC-to-ASCII change would reorder pages that caseworkers know by heart.
Each finding becomes a design decision before code generation. The stored ISN becomes a real foreign key, populated through legacy_isn. The PE group becomes a history table with explicit seq, with the shift-down logic preserved and tested. The report gets an EBCDIC-compatible sort key. Parallel runs then compare every changed record after each nightly batch until differences reach zero.
The nearest public precedent in Modernize Software's case list is an unnamed financial services firm that moved a 500,000-plus-line Fujitsu NetCOBOL application with embedded SQL to C# in under six months. Different language, same pattern: translate code and data access together, then prove equivalence.
Frequently asked questions
1. What is Natural ADABAS used for?
Natural ADABAS runs high-volume transactional systems such as tax, social benefits, insurance policy administration, payroll and logistics. Adabas stores the data in an inverted-list database tuned for fast descriptor searches, and Natural provides the online screens, batch programs and reports. Many installations have run continuously since the 1980s and hold decades of business rules in Natural code.
2. Is ADABAS a relational database?
No. ADABAS is an inverted-list database. It stores records with repeating fields (MU) and repeating groups (PE) inside one record, indexes them through descriptors, and identifies records by ISN rather than by a declared primary key. It does not enforce foreign keys. SQL access is available through add-on products, but the native interface is Natural or direct Adabas calls.
3. Who owns Adabas and Natural now?
Software AG is owned by Silver Lake. In January 2025, Software AG announced that Adabas & Natural and ARIS would operate as standalone businesses with separate management teams under the Software GmbH holding company. The Adabas & Natural business continues to publish product releases, including Natural for z/OS 9.2.4 and Natural for Linux 9.3.3 in October 2025.
4. How do you convert ADABAS MU and PE fields to SQL tables?
Create a child table for each MU field and each PE group, keyed by the parent record and an occurrence number. An MU field inside a PE group becomes a grandchild table keyed by the PE row and its own occurrence number. Keep the occurrence numbers, because Natural code addresses values by position, and many applications treat occurrence 1 as current.
5. What replaces ADABAS descriptors in SQL?
Ordinary and unique descriptors become B-tree and unique indexes. Superdescriptors become composite or expression indexes, and subdescriptors become expression indexes. Null-suppressed descriptors are best modeled as partial indexes plus explicit IS NOT NULL filters in queries. Phonetic and hyperdescriptors need their algorithms re-implemented as functions or computed columns and tested for identical key values.
6. Should we migrate Natural to Java or to C#?
Choose the platform your organization already runs and staffs. Both handle Natural semantics well. Java with Spring suits Linux-centric estates and shops with JVM skills, while C# with EF Core suits Microsoft-centric estates. The hard parts are identical either way: MU and PE mapping, hold semantics, collation and equivalence testing. The data design decides the outcome, not the language.
7. Can Natural programs access SQL databases?
Yes. Natural for Db2 lets Natural programs on z/OS read and update Db2 tables, so some organizations move the data first and keep Natural for a period. That spreads risk but means two migrations. Programs written around Adabas features such as MU and PE fields, ISNs or HISTOGRAM still need changes when the data becomes relational.
Start with a Natural and ADABAS codebase assessment
Modernize Software offers a free codebase assessment that returns a complexity report, an automation estimate and a rough timeline in about five business days, followed by a fixed-price quote. Send your Natural libraries, DDMs and FDTs and you will see exactly where your MU, PE, ISN and hold-logic risks sit. Request your assessment, or review the Natural/ADABAS to Java and Natural/ADABAS to .NET service details first.

info@semanticdesigns.com
