---
title: "Organizing Julia Research Software Projects"
author: "J. Fuhrmann"
date: 2026-06-25
subtitle: |
Slides assembled with the assistance of Claude (Anthropic) from material by J. Fuhrmann at
.
theme: metropolis
fontsize: 10pt
aspectratio: 169
colorlinks: true
---
# Getting Started
## Why Julia for Research Software?
- **Performance**: JIT-compiled, near-C speed without manual optimization
- **Expressiveness**: High-level syntax, close to mathematical notation
- **Package ecosystem**: 10000+ registered packages; best-in-class package manager
- **Reproducibility**: Built-in environment management via `Project.toml` and `Manifest.toml`
- **Interactivity**: REPL, Pluto notebooks, Jupyter support
Julia is evolving — current LTS version: **1.10**, current stable: **1.12**
## Installing Julia: `juliaup`
Use **juliaup** — the recommended installer and version manager
```bash
# Linux / macOS
curl -fsSL https://install.julialang.org | sh
# Windows (PowerShell)
winget install julia -s msstore
```
- Installs Julia and keeps it up to date
- Manages multiple Julia versions side by side
- **Avoid** OS package managers (Homebrew, apt, ...) — versions are often outdated
See:
## Managing Julia Versions with `juliaup`
```bash
juliaup status # list installed versions
juliaup add 1.11 # install a specific version
juliaup default 1.11 # set default version
juliaup update # update all installed versions
julia +1.10 myscript.jl # run with a specific version
```
`juliaup` is analogous to `rustup` (Rust) or `pyenv` (Python)
## Editors and the REPL
**Visual Studio Code** with the Julia extension is the recommended editor
- Integrated REPL, inline output, debugger, profiler
-
**Other editors**: Emacs (`julia-emacs`), Zed, Vim/Neovim
**The REPL** is Julia's interactive shell — keep it running while you work
```
$ julia
julia> # normal mode
] # package manager (Pkg) mode
; # shell mode
? # help mode
```
# Working with Julia Code
## Write Code in Functions, Not Scripts
Running scripts in global scope is a common pitfall:
```julia
# BAD: MyScript.jl in global scope
using Plots
x = 0:0.1:2π # untyped global variable
y = sin.(x) # no JIT optimization possible
plot(x, y)
```
**Problems:** No JIT optimization, precompilation overhead on every run
```julia
# GOOD: wrap in a function
function main(; n=100)
x = range(0, 2π; length=n)
plot(x, sin.(x))
end
```
See: [Julia performance tips — avoid global variables](https://docs.julialang.org/en/v1/manual/performance-tips/#Avoid-global-variables)
## Work from the REPL with `include()`
Start Julia once and reload code without restarting:
```bash
$ julia --project=.
```
```julia
julia> include("scripts/myscript.jl")
julia> main(n=200)
# after editing myscript.jl:
julia> include("scripts/myscript.jl") # reload
julia> main(n=200)
```
With Julia < v1.12, when **redefining a `struct` or a constant**, restart is needed.
## Automatic Reloading with `Revise.jl`
[Revise.jl](https://github.com/timholy/Revise.jl) watches source files and recompiles changes automatically.
**One-time setup** — add to `~/.julia/config/startup.jl`:
```julia
using Revise
```
Then use `includet` (tracked include) instead of `include`:
```julia
julia> includet("scripts/myscript.jl")
julia> main() # uses the current version
# edit myscript.jl ...
julia> main() # automatically sees changes!
```
For packages loaded via `using`, Revise tracks all included files automatically.
## Wrap Scripts in Modules
Avoid name clashes when loading multiple scripts in one session:
```julia
# scripts/myscript.jl
module MyScript
using Plots
function main(; n=100)
x = range(0, 2π; length=n)
plot(x, sin.(x))
end
end # module
```
```julia
julia> includet("scripts/myscript.jl")
julia> MyScript.main(n=200)
```
# Environments and Reproducibility
## What Is a Julia Environment?
An **environment** defines which packages are available for `using` / `import`.
It is described by two files in a directory:
| File | Purpose |
|------|---------|
| `Project.toml` | Direct dependencies + version constraints |
| `Manifest.toml` | Full dependency tree with exact versions |
Julia resolves `using Package` by searching the active environment(s).
## The Global Environment
Default location: `~/.julia/environments/v1.x/`
```julia
julia> using Pkg
julia> Pkg.add("Plots") # installs into global env
```
**Problem with a shared global environment:**
- Package version conflicts between projects
- Hard to share: collaborators must manually figure out what to install
- No reproducibility guarantee
> Use the global environment only for personal development tools
> (e.g. `Revise`, `BenchmarkTools`, `JuliaFormatter`)
## Local Project Environments
Each project gets its **own** environment:
```bash
$ cd MyProject
$ julia --project=.
```
```julia
julia> using Pkg
julia> Pkg.add("DifferentialEquations")
julia> Pkg.add("Plots")
```
This creates `MyProject/Project.toml` and `MyProject/Manifest.toml`.
All package installs stay local to `MyProject` — no conflicts with other projects.
## `Project.toml`: What It Contains
```toml
name = "MyProject"
uuid = "8f4d0f85-..."
version = "0.1.0"
[deps]
DifferentialEquations = "0c46a032-..."
Plots = "91a5bcdd-..."
[compat]
julia = "1.10"
DifferentialEquations = "7"
Plots = "1"
```
- Lists direct dependencies with UUIDs
- `[compat]` constrains acceptable versions
- **Should be committed to version control**
## `Manifest.toml`: Reproducibility
```toml
julia_version = "1.11.0"
manifest_format = "2.0"
[[deps.DifferentialEquations]]
deps = ["BoundaryValueDiffEq", "...]
git-tree-sha1 = "abc123..."
version = "7.13.0"
# ... hundreds more entries ...
```
- Records **exact versions** of every dependency (direct and transitive)
- Enables bit-for-bit reproducible environments
- **Committing it** → anyone can reproduce the exact environment
- **Not committing it** → collaborators get latest compatible versions
## Key Package Manager Commands
```julia
julia> using Pkg
Pkg.activate(".") # activate local environment
Pkg.add("SomePackage") # add a dependency
Pkg.rm("SomePackage") # remove a dependency
Pkg.update() # update all packages
Pkg.status() # list installed packages
Pkg.instantiate() # install all deps from Project.toml
Pkg.resolve() # re-resolve and update Manifest.toml
```
Shortcut: press `]` in the REPL to enter **Pkg mode**:
```
(MyProject) pkg> add Plots
(MyProject) pkg> status
```
## Environment Stacking
Activating a local environment **does not hide** the global one:
```
Active environments (search order):
1. MyProject/ (local) ← project dependencies
2. ~/.julia/environments/v1.11/ (global) ← dev tools
```
- Project dependencies go in the **local** environment
- Development tools (`Revise`, `BenchmarkTools`) stay in the **global** environment
- Clean separation: tools don't pollute the reproducible project environment
# Research Projects vs. Packages
## Packages vs. Research Projects
| | **Package** | **Research Project** |
|:------------------|:------------------------------|:------------------------------------|
| Purpose | Reusable library for others | Produce specific results/paper |
| Users | External users, many versions | Small team, evolves with the work |
| Data | No data | Central: raw data, results, figures |
| Workflow | Stable API, tests, docs | Exploratory, trial-and-error |
| Sharing | Registered in a registry | Shared via git URL |
| Notebooks/scripts | Optional | Core deliverables |
A research project in Julia corresponds to an **application** in the Pkg glossary.
## Challenges of Research Software
Research software tends to be:
- **Exploratory**: requirements change as understanding grows
- **Collaborative**: shared between group members, possibly with different Julia versions
- **Long-lived**: may need to reproduce results years later
- **Mixed**: code + data + notebooks + paper drafts in one place
- **Underdocumented**: under time pressure, tests and docs are often skipped
These challenges make **good organization even more important**, not less.
## Why Organize a Research Project Like a Package?
Structuring a research project as a (non-registered) package gives you:
- **Shared environment** (`Project.toml`) — one `Pkg.instantiate()` sets up everything
- **Shared source code** in `src/` — import project utilities with `using MyProject`
- **Unit testing** via `test/runtests.jl` — catch regressions early
- **Documentation** with Documenter.jl — explain your methods
- **Version control ready** — standard layout, easy to push to git
This is sustainable research software with minimal overhead.
# Structuring the Project
## Recommended Project Layout
```
MyProject/
├── Project.toml ← environment + package definition
├── Manifest.toml ← (optionally committed) exact versions
├── README.md
├── LICENSE
├── src/
│ └── MyProject.jl ← shared project code (a Julia module)
├── scripts/
│ └── simulation.jl ← scripts that produce results
├── notebooks/
│ └── analysis.jl ← Pluto notebooks
├── test/
│ └── runtests.jl ← unit tests
├── docs/ ← Documenter.jl sources
└── papers/ ← manuscripts
```
## Scripts: No Activation Needed
Start Julia from the project root — the environment is active for all scripts:
```bash
$ cd MyProject
$ julia --project=.
```
Scripts need no explicit activation code:
```julia
# scripts/simulation.jl
using MyProject # shared project code from src/
using Plots, DifferentialEquations
function main()
# ...
end
```
```julia
julia> includet("scripts/simulation.jl")
julia> main()
```
## Pluto Notebooks: Disabling the Built-in Package Manager
Pluto has its own built-in package manager — great for standalone notebooks,
but it must be **disabled** to share the project environment.
Add a Pkg cell at the top of the notebook:
```julia
# Pkg cell — triggers Pluto to hand over package management
using Pkg
Pkg.activate(joinpath(@__DIR__, "..")) # activate MyProject environment
```
**Advantages of Pluto for research projects:**
- Reactive: cells re-run automatically on change
- Notebooks are plain `.jl` files — diff-friendly in git
- HTML/PDF export built in
- `Revise.jl` works normally once the built-in manager is disabled
## Sub-packages Inside a Project
Code that grows can become a standalone package inside `packages/`:
```
MyProject/
└── packages/
└── MySubPackage/
├── Project.toml
└── src/
└── MySubPackage.jl
```
Add it as a development dependency:
```julia
pkg> dev packages/MySubPackage
```
Later, `MySubPackage` can be extracted and registered independently —
scripts depending on it don't need to change.
## To Commit `Manifest.toml` or Not?
**Commit it** when:
- Strong reproducibility is required (paper submission, archiving)
- Working with a fixed Julia version in the team
- Subpackages in relative paths (Julia < 1.11)
**Don't commit it** when:
- Team members use different Julia versions
- You want to track that the project works with a range of dependency versions
- Using `[compat]` in `Project.toml` for version management
Starting with Julia 1.11: per-version manifests (`Manifest-v1.11.toml`) make
committing more practical for multi-version teams.
# DrWatson.jl
## DrWatson.jl: What and Why?
[DrWatson.jl](https://github.com/JuliaDynamics/DrWatson.jl) is a scientific project
assistant that helps with:
- **Project-relative paths** — no more hardcoded absolute paths
- **Standardized file naming** — encode simulation parameters in file names
- **Simulation management** — avoid rerunning finished simulations
- **Tagging** — record git commit hash with saved results
```julia
pkg> add DrWatson
```
Designed to work with the project-as-package layout described here.
## DrWatson.jl: Path Helpers
Forget about `joinpath(dirname(@__FILE__), "..", "data")`:
```julia
# start Julia with: julia --project=.
using DrWatson
datadir() # → MyProject/data/
plotsdir() # → MyProject/plots/
srcdir() # → MyProject/src/
scriptsdir() # → MyProject/scripts/
papersdir() # → MyProject/papers/
# Subdirectories:
datadir("sims", "run01") # → MyProject/data/sims/run01/
```
Works from any script or notebook regardless of where it is in the tree.
## DrWatson.jl: Reproducible File Names
Encode simulation parameters in file names automatically:
```julia
params = Dict("N" => 100, "dt" => 0.01, "method" => "rk4")
savename(params)
# → "N=100_dt=0.01_method=rk4"
savename(params; suffix="jld2")
# → "N=100_dt=0.01_method=rk4.jld2"
# Save results:
@tagsave(datadir("sims", savename(params, suffix="jld2")), results)
```
`@tagsave` adds git commit information to the saved data automatically.
## DrWatson.jl: Avoid Redundant Computations
```julia
function run_simulation(params)
# expensive computation...
return results
end
# Run only if output file does not exist yet:
results, path = produce_or_load(run_simulation, params, datadir("sims"))
```
`produce_or_load` checks for an existing output file matching the parameters.
If found, it loads the result; otherwise it runs the function and saves.
Ideal for parameter sweeps and long-running simulations.
# Hands-On: julia-project-skeleton
## julia-project-skeleton
A ready-to-use template that implements all recommendations:
[https://github.com/j-fu/julia-project-skeleton](https://github.com/j-fu/julia-project-skeleton)
Download and unpack [julia-project-skeleton-2.0.0.zip](https://github.com/j-fu/julia-project-skeleton/archive/refs/tags/v2.0.0.zip) from the releases page,
then use the path to the unpacked directory as the template argument.
Generate an initial project version using [PkgSkeleton.jl](https://github.com/tpapp/PkgSkeleton.jl):
```
$ julia
julia> using Pkg
julia> Pkg.add("PkgSkeleton")
julia> using PkgSkeleton
julia> PkgSkeleton.generate("MyProject";
templates=["julia-project-skeleton-2.0.0"])
```
## Setting Up a New Project: Step by Step
Generate the skeleton (see previous slide), then install dependencies:
```bash
$ cd MyProject
$ julia --project
julia> using Pkg; Pkg.instantiate()
```
Then adapt it to your needs:
- Replace demo files with your own scripts and notebooks
- Update `[deps]` in `Project.toml` as needed (`pkg> add ...`)
- Update `authors`, `name`, and `version` in `Project.toml`
- Push to git early — and commit often
## Running Code in the Skeleton
The skeleton ships ready-to-run demos:
```bash
# REPL workflow with live reloading:
$ julia --project
julia> using Revise
julia> includet("scripts/DemoREPL.jl")
julia> DemoREPL.main(dim=3)
# run a CLI script directly:
$ julia --project scripts/demo-cli.jl
# run the demo Pluto notebook (Pluto need not be installed globally):
$ julia --project etc/runpluto.jl notebooks/demo-notebook.jl
```
## Onboarding a Collaborator
```bash
# Clone the repository
$ git clone https://github.com/you/MyProject
$ cd MyProject
# Instantiate — downloads and precompiles all dependencies
$ julia --project
julia> using Pkg; Pkg.instantiate()
# Run a CLI script
$ julia --project scripts/demo-cli.jl
# or use the REPL with live reloading
julia> using Revise
julia> includet("scripts/DemoREPL.jl")
julia> DemoREPL.main(dim=3)
```
No manual package installation, no version hunting — just `instantiate`.
# Summary
## Summary: Key Recommendations
1. **Install Julia via `juliaup`** — easy version management
2. **Write code in functions** — enable JIT optimization
3. **Stay in the REPL** — use `include()` / `Revise.jl` instead of re-running
4. **Use a local environment per project** — `julia --project=.`
5. **Commit `Project.toml`** always; commit `Manifest.toml` when strong reproducibility is needed
6. **Structure the project as a package** — shared env, shared `src/`, tests, docs
7. **Use DrWatson.jl** — path helpers, reproducible file naming, simulation management
8. **Use `julia-project-skeleton`** — get the layout right from the start
## Sustainable Research Software
By following these practices, a Julia research project achieves all five criteria
for sustainable research software (A. Zeller):
| Criterion | How |
|-----------|-----|
| Have a repo | git from day one |
| Anyone can build | `Pkg.instantiate()` |
| Have tests | `test/runtests.jl` |
| Open for extensions | modular `src/` + sub-packages |
| Have examples | `scripts/` + `notebooks/` |
Julia's package management infrastructure makes this largely automatic.
## Outlook: Newer Developments (Julia ≥ 1.11)
Topics for a follow-up talk:
- **`[sources]` in `Project.toml`** — declare in-tree sub-packages by relative path;
sub-package discovery no longer requires committing `Manifest.toml`
- **Workspaces** (Pkg) — manage a set of related packages with a shared
environment; natural fit for multi-package research projects
- **`@main` entry point** — designate a `main(args)` function as the CLI entry point;
cleaner scripts without boilerplate
- **LocalRegistry.jl** — host a private registry for your team or institution;
share registered packages without publishing to General
- **Sub-packages as registered packages** — register while keeping code in the
project tree; switch from `pkg> dev` to `pkg> add` when ready
## References and Further Reading
- **Julia documentation**:
- **Pkg documentation** (environments, `Project.toml`, `Manifest.toml`):
- **Modern Julia Workflows** (G. Dalle, J. Smit, A. Hill, 2024):
- **DrWatson.jl**:
- **PkgTemplates.jl**:
- **julia-project-skeleton**:
- **Basic Workflow**:
- **Project Workflow**:
- **Project Howto**:
- **Markdown source**: