Choosing Flat-File Formats That Age Well
"It's just a CSV." Four words, said at the start of more integration projects than any other sentence, and rarely true in the way the speaker means. Of all the decisions in a file interface, the format is the one that outlives everything else. Servers get replaced, teams turn over, and whole applications are swapped out. The file format sails on, because changing it means coordinating every producer and every consumer at once. A format chosen casually in one afternoon becomes load-bearing for a decade. It is worth choosing like it.
This article gives you the working knowledge that choice needs: the three format families and what each is genuinely good at. It covers the quoting and escaping realities of delimited files told straight. It covers header and trailer records and the control totals that ride in them. And it covers the one cheap habit that keeps every later change survivable: versioning the format from day one. None of it is difficult; all of it is the kind of thing that gets skipped because the file opened fine in a spreadsheet. This article is part of our Files as Integration Glue series. It expands section 3 of the file interface contract, the section with the most engineering inside it.
Three Families, One Job
Every flat-file format in production descends from one of three families. Delimited formats separate fields with a chosen character — comma, tab, pipe. Fixed-width formats give every field an exact character position and length, with no separators at all. Structured text formats carry the field names inside the data itself, in a nested notation a parser reconstructs. That is the JSON and XML style of the world. The same record looks like this in each:
Delimited (CSV):
D,84312,"Rivera, Ana",OPS,214.50,YYYYMMDD
Fixed-width (layout: type 1, id 6, name 20, dept 3, amount 9, date 8):
D084312Rivera, Ana OPS000021450YYYYMMDD
Structured (one record per line):
{"type":"D","id":84312,"name":"Rivera, Ana","dept":"OPS","amount":214.50,"date":"YYYYMMDD"}
All three carry identical information. They differ in what they demand of the parser, how they behave when the data gets awkward, and how gracefully they absorb change. One more thing they share: the transfer layer does not care. A transfer server such as Sysax Multi Server moves and logs the bytes identically whether they are CSV, fixed-width, or structured text. Format discipline lives entirely at the two endpoints, which is exactly why it has to be written down between them.
Delimited Formats: Simple Until the Data Fights Back
Delimited files are the default for good reasons. Humans can read them, spreadsheet applications open them, and every language parses them. A junior admin can inspect one with nothing but a text editor. The trouble starts when the data contains the delimiter — and in real data, it always eventually does. A name written surname-first, a company called "Widgets, Inc.", an address, a free-text note: the comma arrives. An unprepared parser splits one field into two and shifts every field after it. The comma always arrives; the only question is which quarter.
The standard defense is quoting, and here is the reality nobody tells juniors: "CSV" is not one format. There is no single enforced standard, only a widely shared convention. A field containing the delimiter, a double quote, or a line break is wrapped in double quotes. Any double quote inside such a field is doubled. Some producers follow that convention. Some escape with backslashes instead. Some quote every field, some quote none and hope. Two systems can both truthfully say "we do CSV" and be incompatible. The convention in full, worked:
D,84312,"Rivera, Ana",OPS,214.50,YYYYMMDD comma in data: field quoted
D,84313,"O""Neill, Pat",FIN,180.00,YYYYMMDD quote in data: doubled inside quotes
D,84314,Chen Wei,OPS,,YYYYMMDD empty field: delimiters back to back
D,84315,"note line one
continued line two",OPS,95.00,YYYYMMDD quoted line break: legal, one record
across two physical lines
That last example deserves a hard look. A line break inside a quoted field is legitimate under the common convention. It breaks every consumer that reads the file line by line, which is most quick scripts ever written. Your interface contract should either forbid embedded line breaks outright (and the producer strips them on export) or state plainly that records can span lines. In the latter case, the consumer brings a real CSV parser instead of splitting on newlines. The quick script has been temporary for six years.
Three more decisions belong in the spec, because each one has two defensible answers and the two sides must pick the same one. Delimiter: commas collide with data constantly. Tabs and pipes collide far less, which is why data-heavy feeds often go pipe-delimited. But "less" is not "never," so the quoting rule stays regardless. Encoding: UTF-8 is the sane modern answer, but legacy systems still emit older code pages. The byte-order mark — a three-byte prefix some tools write on UTF-8 files — chokes parsers that don't expect it. The code-page background is in character encodings for admins. Say UTF-8, and say whether a BOM is permitted. Line endings and headers: CRLF or LF (see line endings across systems), and whether row one is a column-header row. None of these are hard calls. All of them are calls, and unmade calls get made twice, differently.
Fixed-Width: The Mainframe's Gift That Still Works
A fixed-width format abolishes the delimiter problem by abolishing delimiters. Every field owns exact character positions — name occupies columns 8 through 27, always — and the parser extracts by position, not by searching for separators. A comma in the data is just a character; nothing can collide, because nothing is special.
The price is that the file means nothing without its layout document: the table of field names, positions, lengths, and padding rules. Text fields are conventionally left-justified and padded with spaces; numbers right-justified and padded with zeros. Amounts very often use an implied decimal — the layout says "9 digits, two implied decimal places." So 000021450 means 214.50, and no decimal point ever appears in the file. Sign conventions vary (a leading minus, a separate sign column, and stranger legacy encodings). Dates are digit runs like YYYYMMDD that only the layout can interpret. Get one position wrong and every field after it reads as garbage — perfectly, consistently, on every record. Fixed-width does not fail halfway; it commits.
So why does the family survive? Because within its constraints it is superb. Parsing is trivial and fast in any language, including on platforms with no libraries at all. Record length is constant, so file size predicts record count exactly and truncation is instantly visible. And it is the native tongue of the mainframe and ERP-style world — payroll bureaus, banks, insurers. So when your counterpart says "here is our layout," accepting it is usually wiser than fighting it. The genuine weakness is change: because positions are absolute, widening one field shifts every field after it and breaks every consumer simultaneously. Fixed-width formats age well only when versioned and evolved deliberately — the subject of evolving a file interface.
Structured Text: Self-Describing, at a Price
Structured formats put the field names in the file, so each record announces its own shape. That buys three real advantages. Optional fields are natural (absent means absent — no counting commas). Nesting is possible (an order containing its line items travels as one record). Mature parsers and schema validators exist on every modern platform, so structural validation is a solved problem rather than a homegrown one.
The costs are equally real. Verbosity: every record repeats every field name, tripling raw size — though compression claws most of that back, since repeated names compress almost to nothing. Parsing dependency: the consumer needs a real parser, which the twenty-year-old system on the other side may not have. "Everything can read files" is true, but "everything can parse nested structured text" is not. And the one-big-document trap: a file written as a single giant array must usually be parsed whole. That is memory-hungry at batch sizes, and one syntax error can poison the entire file. The practical fix is the line-delimited variant shown in the first example: one complete object per line. It restores streaming, keeps per-record error isolation, and still validates with standard tools. If you choose structured, put "one record per line" in the contract.
Choose structured when the data is genuinely nested or optional-heavy and both sides are comfortably modern. Choose it, too, when schema tooling matters more than universality. But resist choosing it as a fashion statement for flat tabular data between one modern system and one ancient one. That is how a format decision turns into an integration project. Flat data in a nested notation is a spreadsheet wearing a costume.
Header and Trailer Records, Precisely
Whatever family you choose, the formats that age well share one habit: the file carries its own paperwork. A header record is the first line, and it describes the file rather than the data. It carries a record-type marker, the interface identifier, the business date, and the format version. With a header, a file identifies itself. A feed delivered to the wrong folder, or a Tuesday file arriving Wednesday, is detectable from line one rather than from downstream damage. A Tuesday file arriving on Wednesday should at least have to say so.
A trailer record is the last line, and it proves the file is whole. It carries the record count and one or more control totals. These are numbers computed from the data by the producer and recomputed by the consumer, which must match exactly. The classic trio:
- Record count — the number of detail records. State whether header and trailer are included (convention: they are not). A file truncated in transit fails the count immediately.
- Amount total — the sum of a money column, to the penny. Catches truncation, but also parsing misalignment: if the consumer's parser drifts a column, the amounts stop summing.
- Hash total — the sum of a numeric identifier column, such as account numbers. The result is meaningless as business data, which is the point: it is pure verification. It catches a substituted or duplicated record that count and amount alone might miss.
Here is a complete miniature file with the arithmetic visible:
H,PREMIUM_FEED,YYYYMMDD,02 header: feed id, business date, format version D,100211,214.50 D,100212,180.00 D,100213,95.25 T,3,489.75,300636 trailer: count, amount total, hash total count = 3 detail records amount = 214.50 + 180.00 + 95.25 = 489.75 hash total = 100211 + 100212 + 100213 = 300636
Control totals are logical checks: they verify the records, so they survive re-encoding, line-ending changes, and compression. They complement, not replace, byte-level checksums, which prove the transferred copy is bit-identical to the original. That layer is covered in checksum files and manifests. A well-dressed interface often runs both: the checksum proves the transfer, and the trailer proves the content. The acknowledgment echoes the totals back so the producer knows the consumer read what was sent. That is the round trip described in acknowledgment patterns.
Remember: a trailer only protects you if the consumer verifies it and rejects on mismatch, every run, automatically. A control total nobody checks is decoration. Write the verification into the import job and the rejection behavior into the contract.
The Three Families Side by Side
| Property | Delimited | Fixed-width | Structured |
|---|---|---|---|
| Human readability | Good | Poor without the layout | Good, verbose |
| Awkward data (delimiters, quotes) | Needs strict quoting rules | Immune (positional) | Handled by the notation |
| Parsing effort | Low, but use a real parser | Trivial anywhere | Needs a real parser library |
| Nesting and optional fields | Awkward | No | Natural |
| Tolerance of format change | Moderate (append columns carefully) | Low (positions shift) | High (named fields) |
| Typical home | General business feeds | Banking, payroll, ERP-style suites | Modern app-to-app exchange |
Version the Format from Day One
Every format eventually changes — a new field, a widened amount, a retired code. The difference between a routine change and an outage is whether the format announced its version from the beginning. The mechanism costs one field: a version marker in the header record (the 02 in the worked example above), or a token in the filename. The consumer's first act on every file is to read the version and compare it to the versions it understands. It must reject an unknown version loudly rather than guess. A consumer that guesses parses new data with old assumptions and produces subtly wrong results. A consumer that rejects produces a clean, diagnosable failure and an obvious conversation. I will take the loud rejection every time; the quiet guess surfaces at audit.
Kestrel Payroll collected on that rule one Monday morning. The benefits provider's export team had added a column to the premium feed on Friday afternoon. They bumped the header's version marker from 02 to 03 as the contract required, and went home for the weekend. Kestrel's loader read the header, found a version it did not know, and rejected the file before parsing a single detail record. So the on-call administrator saw a clean "unknown format version 03" in the log. They did not see a ledger full of amounts shifted one column to the right. The two teams settled the new field spec by lunchtime, and the corrected version 03 loaded that afternoon. One field in the header had cost nothing to add and had saved a reconciliation.
Do this on day one even though version 01 is the only version that exists. Retrofitting a version field later is itself a breaking change — the one change you cannot make gracefully. Keep the field spec document versioned in lockstep, referenced from the interface contract. That way, "version 02" names both a file shape and a page both teams can read. The full choreography of moving from one version to the next is the closing article of this series, evolving a file interface without breaking the other side. It covers parallel runs, deprecation windows, and laggard consumers.
A Short Decision Path
When the choice is yours to make, walk this list in order and stop at the first rule that fires:
- Does one side have a native format? Suppose the counterpart's system emits a fixed-width layout it has used forever, or the industry runs on standardized EDI documents (see EDI basics for admins). In that case, take what is native. Fighting a system's mother tongue costs more than learning it.
- Is the data flat and tabular? Delimited, UTF-8, quoted per the common convention. Pick pipe or tab over comma when the data is text-heavy. This is the right answer for most business feeds.
- Is the data nested or optional-heavy? Structured, one record per line.
- Is the consumer ancient, embedded, or unknown? Fixed-width remains the format that parses anywhere, forever.
- Whatever you chose: add the non-negotiable four — header record, trailer record, control totals, version marker. Write dates inside the data as
YYYYMMDDso they sort and parse unambiguously. The reasoning is in datestamp formats that sort.
The format your internal system produces and the format the interface requires are often different, and something must bridge them. Keep that transformation as an explicit step beside the transfer, not buried inside either application. The design argument is made in transforming files between systems. If Sysax FTP Automation handles the delivery, its pre- and post-processing hooks are the natural place to run your transform and validation scripts. That way, the file is checked and shaped immediately before it leaves or right after it arrives, in the same job that moves it.
The Format Outlives You
Choose boring, and choose completely. The choices are a delimited file with honest quoting rules, a fixed-width layout with a maintained document, or line-delimited structured text. Any of them ages well when it carries a header, a trailer, control totals, and a version. None of them ages well without those four. The format will still be crossing the wire when everyone in the room has changed jobs. The kindest thing you can do for your successors is a shape that explains itself and totals that catch its own failures. They will still call it just a CSV, and for once they will be right.
With the format settled, the next question is the return path: how the producer ever learns the file arrived, parsed, and posted. That is acknowledgment patterns, up next in this series.
Frequently Asked Questions
Isn't CSV a standard? Why do CSV files keep breaking?
Why would anyone still choose fixed-width?
What exactly is a hash total?
Is a column-header row the same as a header record?
Which format is best for very large files?
From the Sysax team: we build secure file transfer software for Windows. Sysax Multi Server is an FTP, FTPS, SFTP, and HTTPS server. Sysax FTP Automation handles scheduled, scripted transfers. Free trials are on the download page.
