cswork / libraryOpen practice workspace

Articles

Railway Storage Engineer Interview: Prepare for the Storage-Engine Project and Review

Use Railway's official storage-role process to plan a storage-engine exercise, crash matrix, tests, and clear project-review explanation.

Railway publishes an unusually concrete storage interview prompt: asynchronously “Design a Storage Engine to power something like Railway's Volumes,” then build or expand the solution during a 60-minute interview and review it with the team. That is enough to prepare a meaningful engineering artifact. It is not a specification of Railway's private engine, hidden test suite, or scoring rubric. The most credible preparation is a small implementation whose durability promise, failure behavior, tests, and missing features you can explain under questioning.

The current official listing is headed Senior Infra Engineer: Storage; “Railway Storage Engineer” here is the shorter search phrase. The listing says the role works on a log-structured block-storage system and values storage internals, I/O performance, reliability, and clear communication. It describes an initial discussion, the small project, a team review, further team and CEO conversations, and an offer stage. Process details can change; your invitation is authoritative for your own schedule. This article concentrates on the public project and review. Evidence boundary: Railway's role page is the source for its stated stages; the implementation sketch and test rubric below are original practice, not purported Railway expectations.

Read the public prompt literally, then ask for scope

Railway's page calls for a storage engine “to power something like Railway's Volumes.” The qualifier matters. It does not say “reproduce the production Volumes implementation,” nor does it publicly specify a block size, file-system semantics, replication factor, API, language, or persistence guarantee for the candidate project. The same page invites candidates to ask questions ahead of time. Do that if your invite leaves consequential constraints open: expected interface, permitted dependencies, whether the interviewer will supply an extension request, runtime environment, and how much pre-work is appropriate.

The listing gives the interview structure: approximately 0–5 minutes for introductions, 5–50 for building or expanding the solution, and 50–60 for questions on Railway or technology. It then says the team will review the solution and probe the candidate's problem solving, decomposition, and presentation. This is official process language, not a promise that every interview follows those minute boundaries exactly. Prepare a working baseline that can be discussed and modified, with a short README and tests that run with one command. An impressive architecture slide cannot substitute for an inspectable implementation if the interview asks you to build on it.

Railway's public Volumes reference and backup documentation give product context: persistent state, mounts, and backup/restore concerns. They do not reveal the assignment's intended internal design. Use them to formulate questions—what does a user expect after a deploy or a restore?—not to claim you know how Railway stores every byte. The role page's mention of log-structured block storage does justify preparing log, I/O, and recovery fundamentals.

Choose one narrow, falsifiable practice contract

For rehearsal, build a single-process, local block-like store with write(offset, bytes), read(offset, length), flush(), and reopen(). Fix a maximum logical size and state whether writes may be unaligned or overlapping. State whether reads beyond the written region return zero-filled bytes, an error, or a short result. This is merely one possible exercise, not the project specification Railway will send. A key-value API is another reasonable practice choice if the actual prompt allows it, but explain its translation to block-oriented use rather than assuming they are identical.

Write the contract at the top of the README. Example: “After flush() returns successfully, every preceding accepted write survives process restart and power loss within the assumptions of the underlying filesystem and device; writes not yet flushed may or may not survive. Reads in the running process see the most recently accepted write.” That statement exposes the key boundary. If the API acknowledges each write() as durable, then sync must occur before each success response or the promise must change. If write() only accepts into memory and durability occurs at flush(), document that explicitly. Never call a buffered write() durable because it reached a userspace array.

Two details make this practice artifact more interview-worthy. First, preserve partial-overwrite semantics: if bytes at offsets 10–14 are replaced, unaffected adjacent bytes must stay intact. Second, make errors visible. A short disk write, failed sync, corrupt record, or out-of-range offset should return a controlled error or fail the operation, not silently acknowledge. Design a test for each choice. You can keep concurrency out of the first version, but declare “single writer, no concurrent reads during mutation” as a limitation rather than letting a race masquerade as a guarantee.

One coherent write and recovery path

An append-only practice design can write records containing a version, offset, length, payload, and checksum to a log. Maintain an in-memory index or reconstructed block map for fast reads. A write validates bounds, appends a complete framed record, updates the in-memory view at a well-defined point, and returns according to the chosen acknowledgement contract. flush() asks the operating system to persist the log, checks the result, and only then reports the declared durability boundary. On reopen(), scan complete valid records in order and rebuild the view. A torn or truncated tail after a crash must not be interpreted as a valid overwrite.

The file format needs enough framing to distinguish a complete record from a partial one. A fixed header with length and checksum is a practical teaching choice. Validate lengths before allocating memory or seeking: a corrupt length must not request terabytes or escape the file. Define whether recovery stops at the first invalid tail, reports corruption if an invalid record occurs in the middle, and preserves a diagnostic copy before repair. This distinction prevents a convenient “ignore all errors” replay loop from hiding actual data loss. Test an interrupted header, interrupted payload, wrong checksum, and a valid record after a damaged middle section.

An in-memory index is a performance optimization, not a second source of truth. If it is rebuilt at startup, explain the startup cost as log size grows. Periodic checkpoints or compaction can reduce replay time and disk space, but introduce atomic replacement and crash-ordering complexity. For a small project, it may be stronger to deliver a correct replaying baseline and describe the measured point at which compaction becomes necessary. If you do add compaction, prove that a crash during replacement leaves at least one complete recoverable state. Do not delete the old log before the new snapshot is safely durable under the chosen filesystem assumptions.

The crash matrix is the real demonstration

“It survives restart” is insufficient if your test merely closes the file cleanly. A useful review artifact names interruption points and expected outcomes. In the practice contract above, a crash before any record is appended leaves the old value. A crash during header or payload append can leave a partial tail that recovery ignores or reports, never applies as a valid write. A crash after a complete append but before flush() returns has an uncertain durability outcome: the new value may or may not survive, so the API must not have promised it. A crash after flush() succeeds must recover the new value under the stated storage assumptions. A failed flush() must not yield a success acknowledgement.

Injection pointAllowed recovered valueWhat must never happen
Before appendOld valueAcknowledged new value with no record
Mid-recordOld value or earlier complete stateApply half a payload as a full write
Complete append, before successful syncOld or new, if no durable acknowledgementClaim a guaranteed new value
After successful sync and acknowledgementNew valueReopen to an older value under the stated contract

Simulate these points with a fault-injecting file abstraction or a subprocess killed after specific operations. Distinguish process crash from power loss: a process kill does not prove actual device persistence. You can still test recovery logic deterministically and state that hardware/filesystem guarantees require a different fault setup. The quality signal is the honesty of your contract and test evidence, not a theatrical claim of “zero data loss” from one local run.

Discuss performance without skipping correctness

Railway's role explicitly mentions I/O-layer performance and high-density, highly available storage. In a project review, start by measuring the small engine you built: write latency distribution, bytes written per logical byte, read latency for sequential and random access, startup replay time, and space amplification as updates accumulate. Show the workload and machine configuration next to the numbers. A single throughput figure without sync policy is misleading: batching syncs can improve throughput while changing the durability/latency trade-off.

Prepare to compare alternatives. An append-only log makes recovery and sequential write behavior easy to reason about, but accumulating old versions costs space and replay time. A page-oriented overwrite design may reduce write amplification for some workloads but makes atomic multi-page changes harder. A B-tree, LSM-style approach, or direct block map may be appropriate under different read/write and device conditions. You do not need to implement them all. Explain what the baseline optimizes, what it sacrifices, and what measurement would trigger a redesign.

The role's production system is distributed, but a toy single-node project should not pretend to have solved replication, leader failover, fencing, or online migration. If prompted, expand the guarantee stepwise: identify the authoritative log, sequence writes, define when a replica is caught up, prevent two writers from accepting conflicting histories, and explain backup consistency. Railway's product docs can motivate these questions, but they do not supply a hidden expected answer. A thoughtful boundary between implemented and proposed work is a strength.

Make the team review easy to inspect

Prepare a five-minute walkthrough: contract, file format, write path, recovery path, and one injected failure. Then invite a reviewer to change a requirement. If they ask for concurrent writers, say how you would serialize log append and coordinate reads; if they ask for snapshots, explain a sequence number and durable snapshot boundary; if they ask for large-volume scale, discuss compaction, startup, caching, and measurement. Trace one byte range through the code instead of defending every design choice abstractly.

Keep a short “known limitations” section: single process, no encryption, no replication, no actual power-loss rig, simplified checksums, fixed capacity, or whatever is true. Rank limitations by risk. A candidate who says “my test kills the process, which does not validate device flush under sudden power loss” shows better storage judgment than one who labels a clean reopen test “crash safe.” Because Railway says it will examine problem solving and presentation, this ability to separate demonstrated behavior from intended architecture is directly relevant.

Sources and further reading