Skip to content

Lesson 6.15: Architecture Patterns from Top Teams

🎯 What You’ll Learn

By the end of this lesson you will be able to:

  • Describe four common architecture patterns used by top FRC teams
  • Explain the IO layer pattern and how it enables simulation and replay
  • Understand the superstructure pattern for coordinating complex mechanisms
  • Recognize trigger-based and singleton patterns in real team code
  • Evaluate which patterns might benefit your team’s codebase

Why Architecture Matters

In Units 0–3 and Unit 4, you learned the standard WPILib command-based structure: subsystems control hardware, commands define behaviors, and RobotContainer wires them together. This works well for most teams.

But as robots get more complex — more mechanisms, more sensors, more autonomous routines — the standard structure can become hard to manage. Top teams have developed architecture patterns that scale better. These aren’t replacements for command-based programming; they’re layers on top of it.

Let’s explore four patterns you’ll encounter when reading top team code.


Pattern 1: IO Layers (Hardware Abstraction)

Used by: Team 6328 (Mechanical Advantage) with AdvantageKit

The IO layer pattern separates what a subsystem does from how it talks to hardware. Each subsystem has:

  1. An IO interface — defines the inputs and outputs (sensor readings, motor commands)
  2. A real implementation — talks to actual hardware (TalonFX, SparkMax, etc.)
  3. A simulated implementation — provides fake data for simulation and testing

Structure

// 1. IO Interface — defines what data flows in and out
public interface ShooterIO {
public static class ShooterIOInputs {
public double leftVelocityRPM = 0.0;
public double rightVelocityRPM = 0.0;
public double leftCurrentAmps = 0.0;
public double rightCurrentAmps = 0.0;
}
void updateInputs(ShooterIOInputs inputs);
void setVelocity(double rpm);
void stop();
}
// 2. Real implementation — talks to hardware
public class ShooterIOTalonFX implements ShooterIO {
private final TalonFX leftMotor;
private final TalonFX rightMotor;
@Override
public void updateInputs(ShooterIOInputs inputs) {
inputs.leftVelocityRPM = leftMotor.getVelocity().getValueAsDouble() * 60;
inputs.rightVelocityRPM = rightMotor.getVelocity().getValueAsDouble() * 60;
}
@Override
public void setVelocity(double rpm) {
leftMotor.setControl(new VelocityVoltage(rpm / 60.0));
rightMotor.setControl(new VelocityVoltage(rpm / 60.0));
}
}
// 3. Simulated implementation — no hardware needed
public class ShooterIOSim implements ShooterIO {
private double targetRPM = 0.0;
private double currentRPM = 0.0;
@Override
public void updateInputs(ShooterIOInputs inputs) {
// Simulate flywheel spin-up
currentRPM += (targetRPM - currentRPM) * 0.1;
inputs.leftVelocityRPM = currentRPM;
inputs.rightVelocityRPM = currentRPM;
}
@Override
public void setVelocity(double rpm) {
targetRPM = rpm;
}
}

The Subsystem Uses the Interface

public class ShooterSubsystem extends SubsystemBase {
private final ShooterIO io;
private final ShooterIOInputs inputs = new ShooterIOInputs();
public ShooterSubsystem(ShooterIO io) {
this.io = io; // Could be real or simulated
}
@Override
public void periodic() {
io.updateInputs(inputs);
Logger.recordOutput("Shooter/LeftRPM", inputs.leftVelocityRPM);
Logger.recordOutput("Shooter/RightRPM", inputs.rightVelocityRPM);
}
}

Why IO Layers?

BenefitExplanation
SimulationSwap in the sim implementation and test without hardware
ReplayAdvantageKit can replay logged inputs through the subsystem logic
TestingUnit tests use the sim implementation — no mocking needed
Vendor flexibilitySwitch from TalonFX to SparkMax by writing a new IO class — subsystem logic doesn’t change

Tradeoff

More files per subsystem (3 instead of 1). For a robot with 5 subsystems, that’s 15 files instead of 5. The benefit is worth it for teams that use simulation and replay heavily.


Your team wants to switch the shooter motors from TalonFX to SparkMax. With the IO layer pattern, what do you need to change?


Pattern 2: Superstructure (Centralized Coordination)

Used by: Team 254 (The Cheesy Poofs), Team 1678 (Citrus Circuits)

You explored this in Lesson 6.10 on state machines. The superstructure pattern takes it further: a single class coordinates all non-drivetrain mechanisms.

How It Works

public class Superstructure extends SubsystemBase {
private final Intake intake;
private final Shooter shooter;
private final Turret turret;
private final Climber climber;
private SystemState desiredState = SystemState.IDLE;
public enum SystemState {
IDLE,
INTAKING,
PREPARING_TO_SHOOT,
SHOOTING,
CLIMBING,
ESTOP
}
@Override
public void periodic() {
switch (desiredState) {
case IDLE -> {
intake.stop();
shooter.idle();
turret.holdPosition();
}
case INTAKING -> {
intake.deploy();
intake.runRollers();
shooter.idle();
if (intake.hasNote()) {
desiredState = SystemState.PREPARING_TO_SHOOT;
}
}
case PREPARING_TO_SHOOT -> {
intake.retract();
turret.aimAtTarget();
shooter.spinUp();
if (turret.isOnTarget() && shooter.isAtSpeed()) {
desiredState = SystemState.SHOOTING;
}
}
case SHOOTING -> {
shooter.feed();
if (!shooter.hasNote()) {
desiredState = SystemState.IDLE;
}
}
// ... more states
}
}
// Commands just request state changes
public Command intakeCommand() {
return runOnce(() -> desiredState = SystemState.INTAKING);
}
public Command shootCommand() {
return runOnce(() -> desiredState = SystemState.PREPARING_TO_SHOOT);
}
}

Key Characteristics

AspectDescription
Single source of truthOne class knows what every mechanism should do
Impossible states preventedCan’t intake and climb simultaneously — the enum enforces it
Simple commandsCommands are one-liners that request state changes
Complex coordinationMulti-step sequences are explicit in the switch statement

When to Use It

The superstructure pattern shines when your robot has many interacting mechanisms that need careful coordination. It’s overkill for a simple robot with independent subsystems.


Pattern 3: Trigger-Based Architecture

Used by: Many teams using WPILib 2024+ features

Instead of writing explicit command classes, trigger-based architecture uses WPILib’s Trigger class to wire behaviors directly in RobotContainer. This reduces the number of command files.

Traditional Approach (Separate Command Classes)

// IntakeCommand.java — a whole file for this
public class IntakeCommand extends Command {
private final IntakeSubsystem intake;
public IntakeCommand(IntakeSubsystem intake) {
this.intake = intake;
addRequirements(intake);
}
@Override public void execute() { intake.runRollers(); }
@Override public void end(boolean interrupted) { intake.stop(); }
@Override public boolean isFinished() { return false; }
}
// In RobotContainer:
controller.x().whileTrue(new IntakeCommand(intake));

Trigger-Based Approach (Inline in RobotContainer)

// No separate file needed
controller.x().whileTrue(
intake.run(() -> intake.runRollers())
.finallyDo(() -> intake.stop())
);

Advanced Trigger Composition

// Combine triggers for complex conditions
Trigger hasNote = new Trigger(intake::hasNote);
Trigger shooterReady = new Trigger(shooter::isAtSpeed);
// Auto-feed when note is loaded AND shooter is ready
hasNote.and(shooterReady).onTrue(
feeder.runOnce(() -> feeder.feed())
);
// Rumble controller when note is detected
hasNote.onTrue(
Commands.runOnce(() -> controller.setRumble(RumbleType.kBothRumble, 0.5))
.andThen(Commands.waitSeconds(0.3))
.andThen(Commands.runOnce(() -> controller.setRumble(RumbleType.kBothRumble, 0.0)))
);

Tradeoffs

AspectTrigger-BasedSeparate Command Classes
File countFewer files — logic lives in RobotContainerMore files — each behavior is a class
ReadabilityGreat for simple behaviors, messy for complex onesEach command is self-documenting
ReusabilityHarder to reuse inline lambdasCommands can be reused in multiple places
TestingHarder to unit test inline lambdasEach command class is independently testable
Team workflowOne person edits RobotContainerMultiple people can work on different commands

Your team’s code uses a mix — some inline bindings and some command classes. That’s a perfectly valid approach.


Pattern 4: Singleton Pattern

Used by: Many FRC teams for subsystem access

The singleton pattern ensures only one instance of a subsystem exists and provides global access to it. Instead of passing subsystems through constructors, you access them via a static method.

Standard Approach (Constructor Injection)

// RobotContainer creates and passes subsystems
public class RobotContainer {
private final ShooterSubsystem shooter = new ShooterSubsystem();
private final IntakeSubsystem intake = new IntakeSubsystem();
public RobotContainer() {
// Pass subsystems to commands that need them
controller.y().onTrue(new AutoShootCommand(shooter, intake));
}
}

Singleton Approach

public class ShooterSubsystem extends SubsystemBase {
private static ShooterSubsystem instance;
public static ShooterSubsystem getInstance() {
if (instance == null) {
instance = new ShooterSubsystem();
}
return instance;
}
private ShooterSubsystem() {
// Private constructor prevents external instantiation
}
}
// Commands access subsystems directly
public class AutoShootCommand extends Command {
private final ShooterSubsystem shooter = ShooterSubsystem.getInstance();
public AutoShootCommand() {
addRequirements(shooter);
}
}

Tradeoffs

AspectSingletonConstructor Injection
ConvenienceNo need to pass subsystems aroundMust thread subsystems through constructors
TestingHarder — global state persists between testsEasier — inject mock subsystems
ClarityHidden dependencies — not obvious what a command needsExplicit dependencies in the constructor
SafetyGuaranteed single instanceCould accidentally create duplicates

Most modern FRC teams prefer constructor injection (the standard WPILib approach) because it’s more testable and makes dependencies explicit. But you’ll see singletons in older codebases and some current teams.


You're reading a top team's code and see that their subsystem has a private constructor and a static getInstance() method. What pattern is this?


Comparing the Patterns

No single pattern is “best.” Teams choose based on their robot’s complexity, team size, and experience level.

PatternBest ForTeam SizeComplexity
Standard Command-BasedMost teams, simpler robotsAnyLow
IO LayersTeams using simulation/replay heavilyMedium–LargeMedium
SuperstructureComplex robots with many interacting mechanismsExperiencedHigh
Trigger-BasedSimple behaviors, reducing file countAnyLow–Medium
SingletonQuick access to subsystems (legacy pattern)AnyLow

What Your Team Uses

Open RobotContainer.java and look at how subsystems are created and passed to commands. Your team uses the standard command-based approach with constructor injection — subsystems are created in RobotContainer and passed to commands that need them.

This is a solid foundation. If your team decides to adopt IO layers or a superstructure in the future, you’d build on top of this existing structure, not replace it.


Reading Top Team Code with Architecture in Mind

Now that you know these patterns, you can read top team code more effectively:

TeamPrimary PatternWhere to Look
6328 Mechanical AdvantageIO Layers + AdvantageKitsubsystems/ — each has IO interface, real, and sim implementations
254 The Cheesy PoofsSuperstructuresubsystems/Superstructure.java — centralized state machine
1678 Citrus CircuitsSuperstructure + IO Layerssubsystems/ for IO, Superstructure.java for coordination
3015 Ranger RoboticsTrigger-BasedRobotContainer.java — heavy use of inline trigger composition

When you browse their repos, look for:

  • How many files per subsystem? (1 = standard, 3+ = IO layers)
  • Is there a Superstructure class? (centralized coordination)
  • How complex is RobotContainer? (simple = separate commands, complex = trigger-based)
  • Are subsystems created with new or getInstance()? (constructor injection vs singleton)

Checkpoint: Architecture Patterns
(1) Which of the four patterns does your team's code most closely follow? (2) Pick one pattern your team doesn't currently use — what would be the biggest benefit of adopting it? What would be the biggest cost? (3) If you were starting a new robot project from scratch with your team, which combination of patterns would you choose and why?

Strong answers include:

  1. Current pattern identification — e.g., “Our team uses standard command-based with constructor injection. RobotContainer creates all subsystems and passes them to commands. We have some inline trigger bindings and some separate command classes.”

  2. Thoughtful tradeoff analysis — e.g., “IO layers would let us test subsystem logic without hardware and use AdvantageKit replay. The cost is tripling our subsystem file count and requiring every team member to understand the interface pattern. For our 4-person programming team, that’s a significant learning curve.”

  3. Practical recommendation — e.g., “I’d use standard command-based as the foundation, add IO layers for the drivetrain and shooter (our most complex subsystems), and keep simple subsystems as single files. I wouldn’t use a superstructure unless our robot had 5+ interacting mechanisms.”


Key Terms

📖 All terms below are also in the full glossary for quick reference.

TermDefinition
IO LayerAn architecture pattern that separates subsystem logic from hardware communication using interfaces, enabling simulation and hardware swaps
Hardware AbstractionThe practice of hiding hardware-specific details behind a common interface so code can work with different hardware implementations
SuperstructureA centralized coordinating class that manages all non-drivetrain subsystem interactions using a state machine
Trigger-Based ArchitectureAn approach where behaviors are wired using WPILib Trigger composition directly in RobotContainer instead of separate command classes
Singleton PatternA design pattern ensuring only one instance of a class exists, accessed via a static getInstance() method
Constructor InjectionPassing dependencies (like subsystems) through a class’s constructor, making dependencies explicit and testable
Dependency InjectionThe broader principle of providing a class’s dependencies from outside rather than having the class create them internally

What’s Next?

You’ve now surveyed the major architecture patterns used by top FRC teams. In Lesson 6.16: Advanced PathPlanner, you’ll explore advanced autonomous features — on-the-fly path generation, custom constraints, zone behaviors, and AutoBuilder composition — taking your autonomous routines to the next level.