How to structure an FRC command-based robot project the right way — subsystems, commands, RobotContainer, and Constants — verified against official WPILib docs.
to read
words
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.
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."
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.
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:
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.
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.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.
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 finishrun() — do something repeatedly until interruptedstartEnd() — one action on start, another on endwaitSeconds() / waitUntil() — timing and conditionsWhen 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.
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 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:
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.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.
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:
Constants.DriveConstants, Constants.ArmConstants.public static final (in C++, constexpr) so they are globally readable and immutable.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."A good Constants file is the first thing you open when tuning, and the only place a port or gain should ever be defined.
Robot.java. If teleopPeriodic() is reading joysticks and setting motors, you have skipped the framework entirely. Move it into subsystems and commands.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.Constants.static mutable state. Keep hardware private; keep subsystems private in RobotContainer.A little repository discipline makes the architecture pay off:
.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 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.DriveSubsystem or vision helper can move to next year's project with minimal edits. The cleaner the boundary, the more you keep.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.
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.
This article is AI-assisted: drafted from primary sources, then reviewed and edited by hand. Errors still get through. When one is reported we fix it and write down what changed — publicly, in the corrections log.
Keep going
You came here for one answer. These lessons teach the same subject properly, in the order a team actually learns it. All 51 are free and none of them need an account to read.
Go deeper
LearnFRC has 394 free FRC lessons across every department. Create a free account to save your place, track your progress, and earn a certificate.
Create a free accountKeep reading
Structured lessons and quizzes across every department. Create a free account to save your progress, track your team, and earn a certificate.