Skip to content
FRC Article 13 min read

FRC Code Structure Best Practices: Command-Based Project Architecture

How to structure an FRC command-based robot project the right way — subsystems, commands, RobotContainer, and Constants — verified against official WPILib docs.

to read
13 min

to read

words
2,369

words

sections
13

sections

Good robot code is not the code that works on the practice field in week six. It is the code a rookie can open in November, understand in an afternoon, and safely change without breaking three other mechanisms. Architecture is what makes that possible. This guide walks through how experienced FRC teams structure a command-based robot project — the packages, the subsystems, the constants, and the wiring — and every structural claim here is drawn from the official WPILib documentation, not folklore.

Why architecture matters more than clever code

FRC has a brutal constraint most software projects do not: you rebuild almost everything, every year, in about six weeks, mostly with students who are still learning. Two things fall out of that.

First, reusability compounds. WPILib's own docs describe command-based code as "clean, extensible, and (with some tricks) easy to reuse from year to year." A drivetrain subsystem, a vision helper, or an auto-routing setup written cleanly in one season can carry over to the next with minor edits. Spaghetti cannot.

Second, structure is a debugging tool. When your intake misbehaves, a well-organized project tells you exactly one file to open. In a tangled one, the intake logic is smeared across Robot.java, three commands, and a static variable somebody added at 2 a.m. Architecture is the difference between "the bug is in IntakeSubsystem" and "the bug is somewhere."

Two frameworks: TimedRobot vs. command-based

WPILib ships two ways to write a robot program, and it is worth knowing where each fits.

  • TimedRobot is the base: a simple loop that calls robotPeriodic(), autonomousPeriodic(), teleopPeriodic(), and so on, once every scheduler cycle (20 ms by default). It is transparent and fine for a very simple robot, but as mechanisms multiply you end up hand-writing state machines and manually preventing two pieces of code from fighting over the same motor.

  • Command-based is a design pattern built on top of that loop. It is the framework WPILib's documentation centers on and the one the large majority of competitive teams use, because it solves the coordination problem for you.

This article is about command-based, because that is where "project structure best practices" actually lives.

The core mental model: what vs. how

Command-based programming is, in WPILib's words, a form of declarative programming: you "specify desired behaviors rather than implementing iteration-by-iteration logic." You say the robot should run the intake while this button is held, not every 20 ms, check the button, and set the motor.

Three concepts carry the whole paradigm:

  • Subsystems are "independently-controlled collections of robot hardware" — the drivetrain, the intake, the arm, the shooter. A subsystem owns its motors and sensors and exposes what the rest of the code is allowed to do with them. This is the "how."
  • Commands are the actions: "Commands run when scheduled, until they are interrupted or their end condition is met," and they are "recursively composable" so you can build complex routines out of simple pieces. This is the "what."
  • Triggers poll a condition — a button press, a sensor reading, a match phase — and schedule a command when it becomes true.

The payoff is separation of concerns: command-based architecture "elegantly separates 'what' actions to perform from 'how' hardware implements them, allowing subsystem modifications without affecting higher-level robot logic." You can rewire the arm from one motor to two, or swap a sensor, and the commands that use the arm never change.

The canonical project structure

A stock WPILib command-based project has a small, predictable skeleton. The root package contains four classes, plus two sub-packages:

frc/robot/
├── Main.java            // entry point — you almost never touch this
├── Robot.java           // extends TimedRobot; runs the CommandScheduler
├── RobotContainer.java  // the wiring hub — subsystems, bindings, autos
├── Constants.java       // all globally-shared constants
├── subsystems/          // one file per subsystem
│   ├── DriveSubsystem.java
│   ├── IntakeSubsystem.java
│   └── ArmSubsystem.java
└── commands/            // multi-subsystem / stateful commands
    ├── AutoScoreCommand.java
    └── ...

The discipline that keeps this healthy:

  • Robot.java stays thin. Its main job is to run CommandScheduler.getInstance().run() in robotPeriodic(). Resist the urge to put mechanism logic here — that is exactly the coupling command-based exists to prevent.
  • RobotContainer is the setup hub. WPILib describes it as the class that "holds robot subsystems and commands, and is where most of the declarative robot setup (e.g. button bindings) is performed."
  • subsystems/ and commands/ hold your mechanism classes and your reusable command classes respectively.

Subsystems, done right

A subsystem is "an abstraction for a collection of robot hardware that operates together as a unit." Get four things right and your subsystems will stay clean all season.

1. Keep hardware private. Motor controllers, encoders, and sensors should be private fields, "exposed only through public methods." Nothing outside the subsystem should be able to grab a motor and command it directly. That single rule is what lets you change wiring, gearing, or sensor type later without hunting through the whole codebase.

2. Expose intent, not implementation. Instead of a public getMotor(), expose setSpeed(double) or, better, command factories (more below). Whether you expose plain control methods or command factories, WPILib's advice is that "consistency matters more than choosing one approach" — pick a convention and hold to it.

3. Use periodic() for the right things. Every subsystem's periodic() method "executes once per scheduler cycle (typically every 20 ms)." Use it for telemetry, logging, and housekeeping that should always happen — not for control logic that belongs to a command. (There is also a simulationPeriodic() for simulation-only updates.)

4. Use default commands for background behavior. A default command runs automatically "when no other command uses the subsystem," which is perfect for things like drive from the joysticks whenever nothing else is driving or hold the arm at its setpoint. WPILib's one caution: try to be consistent about whether "background" behavior lives in a default command or in periodic(), rather than mixing both arbitrarily.

The engine underneath all of this is resource management. The scheduler enforces that no more than one command requiring a given subsystem runs at once; if a new command needs a busy subsystem, the scheduler either interrupts the current one or ignores the new one based on the interruption setting. This is why two commands can never quietly send your arm motor conflicting outputs — a class of bug that plagues hand-rolled loops.

Commands, done right

A command has four lifecycle methods, and knowing what belongs in each keeps them clean:

  • initialize() — called once when the command is scheduled; put the mechanism "in a known starting state."
  • execute() — called every cycle while scheduled; do the continuous work here, "such as updating motor outputs to match joystick inputs."
  • end(boolean interrupted) — called once when the command ends; "wrap up" state, e.g. set motors back to zero. The parameter tells you whether it finished normally or was interrupted.
  • isFinished() — checked after each execute(); "as soon as it returns true, the command's end() method is called and it ends."

Every command also declares its requirements — the subsystems it controls. This "backs the scheduler's resource management mechanism," which is how the scheduler knows the command conflicts with anything else needing the same subsystem.

The biggest structural win: stop writing subclasses. New teams instinctively make a class for every action, and drown in files. WPILib is blunt that a trivial command written as a subclass can be "over 20 lines of boilerplate code." The library provides factory methods that, "through the use of lambdas... can cover almost all use cases," and "teams should rarely need to write custom command classes":

  • runOnce() — do something once and finish
  • run() — do something repeatedly until interrupted
  • startEnd() — one action on start, another on end
  • waitSeconds() / waitUntil() — timing and conditions

When you should write a command class: reserve it for stateful commands that need internal fields (a PID controller with memory, a multi-step sequence with its own counters) or genuinely "complex logic" where a class reads more naturally than a chain of lambdas.

The command-factory pattern and dependency injection

This is the single most important organizational decision in a modern command-based project, and WPILib gives clear guidance.

Single-subsystem actions belong to the subsystem. For a command that only touches the intake, "it makes sense to put a runIntakeCommand method as an instance method of the Intake class." Because the method lives inside the subsystem, it can implicitly require this, which "promotes cleaner dependency injection and reducing boilerplate." Your RobotContainer then reads like plain English: intake.runIntakeCommand().

Multi-subsystem actions belong in a separate class or factory — a command that coordinates drivetrain plus arm plus intake should not live inside any one of those subsystems.

Inject subsystems through constructors — never into other subsystems. When a command needs a subsystem, pass it in through the constructor. But do not pass subsystems into other subsystems: WPILib warns that "passing in other subsystem objects is unintuitive and can cause race conditions and circular dependencies, and thus should be avoided." If two mechanisms must coordinate, do it in a command that requires both, not by having one subsystem hold a reference to the other.

RobotContainer: the wiring hub

RobotContainer is where the pieces connect, and it has one structural rule worth tattooing on the team wall: declare subsystems as private fields.

The modern framework declares subsystems as private members of RobotContainer. WPILib calls this "much more-aligned with agreed-upon object-oriented best-practices" than the old global-subsystem style, because private declaration "prevents uncontrolled access throughout the codebase, maintains clear control flow, and preserves the resource-management system's effectiveness." No more static subsystems reachable from anywhere.

RobotContainer typically does three things:

  1. Instantiate subsystems as private fields.
  2. Bind triggers to commands — usually in a configureBindings() method. With a controller, this looks like driverController.a().onTrue(intake.runIntakeCommand()), where the button object is a Trigger that schedules the command on press.
  3. Provide the autonomous command via a getAutonomousCommand() method, often selected from a chooser on the dashboard.

Keeping all of this in one place means a teammate can answer "what does button X do?" by reading a single method.

Constants: one home, well-organized

Scattered magic numbers are a top source of FRC bugs — the same gear ratio typed slightly differently in two files, a port number no one can find. The Constants class fixes this. It holds "globally-accessible robot constants (such as speeds, unit conversion factors, PID gains, and sensor/motor ports)." Best practices from the docs:

  • Group by subsystem or mode using inner classes. WPILib recommends separating constants "into individual inner classes corresponding to subsystems or robot modes, to keep variable names shorter" — e.g. Constants.DriveConstants, Constants.ArmConstants.
  • Make them public static final (in C++, constexpr) so they are globally readable and immutable.
  • Static-import the inner class in files that use it, so DriveConstants.kMaxSpeed can be referenced directly. WPILib notes an import static "imports the static namespace of a class... so that any static constants can be referenced directly."
  • Bake units in. Store values in consistent units (WPILib's units library or a documented convention) and put conversion factors here too, so a port number and a meters-per-rotation factor never get confused.

A good Constants file is the first thing you open when tuning, and the only place a port or gain should ever be defined.

Common anti-patterns to avoid

  • Logic in Robot.java. If teleopPeriodic() is reading joysticks and setting motors, you have skipped the framework entirely. Move it into subsystems and commands.
  • The god subsystem. One RobotSubsystem that owns every motor defeats resource management — the whole robot becomes a single resource, so only one command can run at a time. Split by mechanism.
  • Subsystems holding other subsystems. As above: this breeds circular dependencies and race conditions. Coordinate in commands.
  • A class for every action. Prefer inline/factory commands; reserve classes for stateful or complex logic.
  • A file for every command group. WPILib cautions against the opposite extreme too — creating "a new file for every single command group, even when the groups are conceptually related." Group related inline compositions together.
  • Magic numbers in code. Every tunable value goes in Constants.
  • Public/static mutable state. Keep hardware private; keep subsystems private in RobotContainer.

Repo hygiene and year-to-year reuse

A little repository discipline makes the architecture pay off:

  • Use version control from kickoff. Git, one branch strategy the team actually follows, and commits small enough to bisect when something breaks.
  • Commit the right things. WPILib projects generate a .gitignore — keep build output (build/, .gradle/) out of the repo, but do commit vendordeps/ (the vendor library JSON files) so teammates build against the same versions.
  • Deploy files, not hardcoded paths. Files placed in the project's deploy directory (e.g. path-planner trajectories, config) ship to the roboRIO and are read at a known runtime location, instead of paths that only exist on one laptop.
  • Design for reuse. Because subsystems hide their hardware behind public methods, a clean DriveSubsystem or vision helper can move to next year's project with minimal edits. The cleaner the boundary, the more you keep.

A note on Python and C++

The structure is language-agnostic. In C++, the same four-file layout applies, with constexpr constants and header/source separation. In Python (RobotPy), you get the same RobotContainer, subsystems, and commands, with a constants.py module standing in for the Constants class. The mental model — subsystems own hardware, commands express behavior, triggers schedule them, RobotContainer wires it together — is identical across all three.

The one-paragraph version

Put every motor and sensor inside a private-fielded subsystem that exposes intent through methods or command factories. Express behavior as commands — inline factories for the simple cases, classes only when you need state. Declare your requirements so the scheduler can keep two commands off the same subsystem. Wire it all together in RobotContainer with private subsystems, trigger bindings, and an auto selector. Put every number in a grouped Constants file. Keep Robot.java thin. Do that, and next November your code will still make sense — to you, and to the rookies you hand it to.

Spot an error or something out of date?Create a free account to suggest an edit

Keep reading

More from the pit

Start learning FRC — free

Structured lessons and quizzes across every department. Create a free account to save your progress, track your team, and earn a certificate.

394lessons
11departments
100%free