Skip to content
Architecture Programming

Single-File Tool Architecture

By Romi Fadli, S.Kom. 8 min read Software Design
On this page
A single file card holding six sections in a fixed order — Config, Helpers, Reporter, Steps, Summary, Entrypoint — with arguments, a config file and a prompt flowing in from the left and exit codes 0, 1, 2 and 130 flowing out to the right

Single-File Tool Architecture is the shape I give to small tools: one file that does one job, with its sections always in the same order and a fixed contract for how it is started and how it reports the result. It is my own convention, not a design from a book. It grew out of writing many small tools that check servers, generate deployment stacks, and convert files.

It is one of the architectures I use, and it covers the smallest end of my work. For services that hold real business rules and live for years I use something heavier; for a tool that does one thing and finishes, this is enough.

At a glance

OriginMy own convention, shaped by many small command-line tools
I use it forTools with one clear job, run by a person or by another script: readiness checks, stack generators, converters
I do not use it forLong-lived services with many use cases
LanguagesPython, Bash
Where Single-File Tool Architecture sits among the architectures I use.

The idea in brief

A. One file, one job

The whole tool lives in a single script. Next to it there are only the things it reads: a dependency list, an example configuration file, and for generators a folder of templates. There is no package, no import between files of the same tool, and nothing to install beyond its dependencies.

B. Read from top to bottom

The file is divided into sections marked by a banner comment, and the sections always come in the same order: configuration, helpers, a reporter, the steps that do the actual work, a summary, and the entry point. Reading the file from top to bottom follows the life of one run.

C. A contract at the top

The first lines of the file say what the tool does and what its exit codes mean. The exit code is the number a program returns when it ends, and it is how another script tells success from failure without reading the output.

D. Three ways in

A tool receives its input in up to three ways: command-line arguments for options, a configuration file for everything that describes one target, and an interactive prompt for values that change on every run. Each kind of value has one way in, so a run is easy to repeat.

Why I chose it

Layers make a small thing complicated. A tool with one job does not need domains, use cases, and adapters. Splitting a few hundred lines across several folders would make the structure bigger than the program, and I would spend more time navigating it than reading it.

One file is easy to copy and move. I can carry a tool to another machine as one script, its configuration, and its dependency list. There is no package to build or publish before it can run somewhere else.

The same order in every tool tells me where things are. Because every tool keeps its sections in the same order, I know where the configuration is validated or where the summary is printed in a tool I have not opened for months, before I read a single line of it.

Another script can call it. Fixed exit codes, and inputs that can come from arguments or from the environment, mean a tool I run by hand today can be run by a scheduler or a pipeline tomorrow without changes.

My conventions

A tool of mine that says it follows this architecture has this shape. In Python:

python
#!/usr/bin/env python3
"""What the tool checks or does.

Exit code:
    0 - every check passed
    1 - a check failed
    2 - the configuration is missing or invalid
  130 - cancelled by the user
"""

# --- Config ------------------------------------------------------------
# --- Helpers -----------------------------------------------------------
# --- Reporter ----------------------------------------------------------
# --- Steps -------------------------------------------------------------
# --- Summary -----------------------------------------------------------
# --- Entrypoint --------------------------------------------------------
SectionWhat it holds
Configloading the configuration file and validating every field strictly, before any connection is made
Helperssmall functions the steps share, such as turning an error into one readable line
Reporterprinting one line per check, pass or fail, and collecting facts for the summary
Stepsthe actual work, one section per part of the job
Summarya count of passed checks and a final status
Entrypointparsing arguments, running the steps, and choosing the exit code
The fixed order of sections in a single-file tool.

Each kind of input has its own way in:

InputUsed forExample
command-line argumentoptions that change how the tool runs--timeout 30
configuration fileeverything that describes one target, one file per target--config server-a.json
interactive promptvalues that change on every runthe address a test message is sent to
Which kind of value enters the tool through which way.

A missing third-party dependency also ends the run with exit code 2 and a message that says how to install it. Next to every tool there is an example configuration file with placeholders, and the real configuration files, which hold credentials, are never committed.

The Bash variant is used for generators — scripts that create a ready-to-use folder from templates. Its header lists the environment variables it respects, and every input is read the same way: from the environment if it is set, otherwise from a question with a default.

bash
# ask VAR "Prompt" "default": use the environment variable if set, otherwise ask
ask() {
    local var="$1" prompt="$2" default="$3"
    [[ -n "${!var:-}" ]] && return
    read -r -p "$prompt [$default]: " input || true
    printf -v "$var" '%s' "${input:-$default}"
}

That one rule lets the same generator run interactively for me and unattended in automation, with one more variable to skip every confirmation. The templates sit in a folder next to the script, so the generator carries everything it needs.

How I check a tool against it

These are the questions I ask a finished tool, taken from the conventions above:

AreaQuestion I askSource
ContractDo the first lines say what the tool does and what each exit code means?the contract at the top
OrderAre the sections in the fixed order, each marked by a banner?the section order
ConfigurationIs every field validated before anything is connected, with its own exit code when it fails?the Config section
InputDoes each kind of value have exactly one way in?the three ways in
ReportingDoes every check print one line, and does the summary count them?the Reporter and Summary
SecretsIs there an example configuration with placeholders, and is the real one kept out of the repository?configuration files
AutomationCan it run without a person, through arguments, configuration, or environment variables?the Bash variant and exit codes
The questions I check a single-file tool against.

Where I do not use it

When a tool grows into a service

A tool that starts to hold several jobs, serves requests, or needs to live and be maintained for years has outgrown one file. At that point the layers that would have been a burden become worth their cost, and I move it to Clean Architecture, where its rules are kept apart from the tools around them.

References

This architecture has no book behind it; it is a convention I built up from my own tools. Two parts of it follow long-standing conventions of Unix command-line programs: an exit code of 0 for success and a non-zero code for failure, and 130 for a run interrupted with Ctrl+C, which shells report as 128 plus the number of the interrupt signal.

Terms

Exit code
The number a program returns when it ends. Scripts use it to tell success from different kinds of failure without reading the output.
Configuration file
A file, here JSON, that describes one target the tool works on, such as one server and its credentials.
Interactive prompt
A question the tool asks in the terminal while it runs, for values that change on every run.
Environment variable
A named value set outside a program and readable by it, which lets a script supply answers without typing them.
Generator
A script that creates a ready-to-use folder, such as a deployment stack, from templates and a few answers.

About the author

Photo of Romi Fadli

Romi Fadli, S.Kom.

Solution Architect · Backend Engineer · Embedded Systems

In my day job I work as a solution architect. I design the system, lead the team that builds it — quality assurance, backend developers, a UI developer, DevOps and DevSecOps — and write the most critical modules myself, such as a certificate authority, a long-term PDF signer, and the core API of a personnel system.

I work across the whole stack that a running system actually needs. On the hardware side that means PCB design, microcontrollers, and embedded firmware — including RISC-V boards running FreeRTOS and Embassy, speaking MQTT over mutually authenticated TLS on a microcontroller with 64 KB of SRAM.

Most of my day-to-day work is software: Rust backends and API modules built on Clean Architecture, an OAuth2/OpenID Connect authorization server written from scratch, PAdES document signing, and certificate authorities — root CAs, timestamping authorities, and OCSP responders. I also build the frontends that sit on top of them.

Running all of that means infrastructure: Docker Compose stacks, networking, Mikrotik and RADIUS, observability with OpenTelemetry, and a steady habit of writing command-line tools and utilities for Linux when the existing ones do not fit.

Share

Enjoyed this?

Get an email when a new article is published.

Referenced by

A stack of Markdown documents and an orange arrow leading to a browser page with a side menu, a heading, paragraphs, a dark code panel, and a table of contents with its active entry highlighted

A single script that turns a project's Markdown documentation into a static site that opens from a folder with no server or internet: a menu, a table of contents that follows the reader, tables that never truncate, code with a copy button, and links that keep working.

7 min read Read more →
An old layout with a grey banner and a new structure with a green banner, each holding a blue database, a purple document store, and an orange telemetry store; three coloured paths pass through one export package marked with a green check

Four commands that move a stack from its old layout into a new structure across major versions: export without changing the source's data, import each store the way its version allows, verify what really arrived, and rename modules while moving their data along.

11 min read Read more →
Three image layer stacks, each with a version tag of its own colour, blue, orange, and green, each leading to an archive file banded in the same colour

Commands that version every module of a stack on its own, build its image under a tag that means exactly one build, refuse to reuse a version or swallow a failed compile, and archive images by name and tag so versions never overwrite each other.

7 min read Read more →
Four identical grey keys on the left each become a key of its own colour on the right, every new key marked with a green check; below, a purple key stays behind a red bar

One command that rotates an environment's internal credentials key by key: a dry run by default, a backup first, a unique value for every key, a real login to prove each credential that can log in, and rollback for any that fail.

8 min read Read more →
A browser window sends one orange path into a single web server, which routes it on to the orange staging environment; dashed paths to the blue development and green production environments stay inactive

Two commands that keep three environments apart on one machine and one web server: one sets an environment's addresses and aligns every file that stores them, rendering nginx from templates and refusing on clashes; the other points the browser at one environment.

11 min read Read more →
A staging configuration with an orange banner and a production configuration with a green banner; ordinary keys cross over with arrows, while a locked secret is stopped halfway by a red cross and production keeps its own lock

Three commands that take a tested release into production without overwriting what production owns: they promote configuration structure but keep its secrets and addresses, refuse to copy new secrets, and apply schema migrations the images carry.

10 min read Read more →
Three sources on the left — configuration, container images, and a backup — flowing into one staging environment with an orange banner, its services, and a healthy check mark

Three commands that build a full copy of production for one release — configuration from the repository, images from the registry, data from the latest backup — restore it in order, run the release's migrations against that data, and remove it without a trace.

13 min read Read more →
A database, an object storage bucket, and a telemetry store on the left, each flowing into a stack of restore points on the right, the front one showing a timeline and a check mark

Three commands that back up a running Docker stack — PostgreSQL, MinIO, and Elasticsearch — without downtime, restore it to any point, and keep retention safe, with exit codes that say how much a failure matters.

12 min read Read more →
A checklist card on the left, a three-part token in the middle with a check mark, and an identity provider marked with a key on the right

An interactive Python script that proves an identity provider is ready for a client: it rehearses a full PKCE sign-in and sign-out like an application, then checks the tokens it receives against the provider's keys the way an application must.

11 min read Read more →
A terminal window listing passing checks, connected to a blue storage bucket with a padlock and a green storage bucket without one.

A single Python script that proves S3-compatible storage is ready before an application depends on it, by using every bucket the way a real client does and catching the failures that return HTTP 200.

12 min read Read more →
Four concentric circles with arrows coming in from a database, a browser window, and a message queue, all pointing toward the centre.

Clean Architecture

Programming

Clean Architecture is one of the architectures I use: why I chose it, the folder and naming conventions every module of mine follows across Rust, Python, Dart, Node.js and C, the questions I check a module against, and where I use something else instead.

12 min read Read more →

Related Articles

A stack of Markdown documents and an orange arrow leading to a browser page with a side menu, a heading, paragraphs, a dark code panel, and a table of contents with its active entry highlighted

A single script that turns a project's Markdown documentation into a static site that opens from a folder with no server or internet: a menu, a table of contents that follows the reader, tables that never truncate, code with a copy button, and links that keep working.

7 min read Read more →
An old layout with a grey banner and a new structure with a green banner, each holding a blue database, a purple document store, and an orange telemetry store; three coloured paths pass through one export package marked with a green check

Four commands that move a stack from its old layout into a new structure across major versions: export without changing the source's data, import each store the way its version allows, verify what really arrived, and rename modules while moving their data along.

11 min read Read more →
Three image layer stacks, each with a version tag of its own colour, blue, orange, and green, each leading to an archive file banded in the same colour

Commands that version every module of a stack on its own, build its image under a tag that means exactly one build, refuse to reuse a version or swallow a failed compile, and archive images by name and tag so versions never overwrite each other.

7 min read Read more →