Skip to main content

Designing a Swim Training Log: Workflows, Fields, and Configuration

Building a swim training app means deciding what a set is, what data it holds, and how workouts evolve. Here's the messy reality behind that meta-model.

Beyond the Stopwatch: What a Swim Workout Really Is

Every swimmer knows the drill: you write '10x100 free on 1:30' in a notebook, then try to remember what it meant three weeks later. For a coach, that same line carries structure—distance, pace, stroke, rest, effort. But if you're building software to track this, the real question isn't what to call the field. It's how to define a workout so that every swimmer, every squad, and every season can use it without the whole system collapsing into a mess of custom spreadsheets.

I've spent months thinking about this while designing a training log for competitive swimmers. The surface request sounds simple: 'When I mark a set as completed, I want to record my split times.' But that one ask drags in a dozen deeper decisions. What exactly is a 'set'? What data does it hold? What happens when a coach changes the main set mid-practice? And how do you keep historical data intact when you tweak the training plan?

This is the meta-model problem. It's not about building a better input form. It's about deciding how a swim workout should be defined, stored, and evolved.

Six Questions Every Workout Model Must Answer

Before writing any code, I broke down what a workout item needs to answer. Six questions, to be exact.

  • What is this object? A set, a drill, a kick set, a pull set—each has an identity.
  • What data can it hold? Distance, pace, stroke, rest, heart rate, perceived effort, notes.
  • How do swimmers interact with it? The form for logging a set differs from the one for viewing a workout summary.
  • How can it change over time? A set goes from 'planned' to 'in progress' to 'completed'—and maybe 'adjusted' if the coach changes it mid-practice.
  • How does it connect to other objects? A set belongs to a workout, which belongs to a practice, which belongs to a season. Sets can also be linked to specific swimmer goals.
  • Where do these rules apply? A junior squad might log distance only, while an elite group tracks stroke rates and split times.

These six layers can't be mashed into one giant configuration table. The type defines the object. The attributes define the facts. The layout defines the interaction. The workflow defines the allowed changes. The relationship model defines connections. And the scope defines who sees what. When you separate these, you can reuse a 'distance' attribute across sets, drills, and even race results without duplicating it.

Workout Types: More Than a Label

Early on, I thought a workout type was just a name and an icon. 'Set' versus 'Drill'—what's the big deal? But it turns out the type is the anchor for everything else. It's the stable identity that other configurations hang onto.

Here's the trap: you might be tempted to put a default 'coach' or 'swimmer' attribute on the type itself. Don't. The default owner isn't part of the type's identity. For a junior squad, the default might be the lane leader. For a varsity team, it's the head coach. Those are attribute rules, not type definitions. If you bake them into the type, you'll end up with thirty different 'Set' types because each squad has a different default.

The same goes for workflow. A type can reference a workflow, but the workflow itself—the states, the transitions, the validation—belongs to its own layer. The type just says 'this is a set.' The workflow says 'a set can be planned, started, completed, or voided.'

Attributes Over Fields: The Unified Form Model

Most training apps let you add 'custom fields' to a workout. That sounds friendly, but it limits you to text boxes and dropdowns. A real swim workout is richer than that.

Take the concept of 'distance.' In a form, you might enter a number. But the system needs to know it's a distance in meters or yards, that it can be summed across sets, and that it should appear in a weekly total. That's not a field—it's an attribute with a data type, a unit, and rules for aggregation.

So I moved to a model where everything is an attribute, and attributes compose forms. Here are the types I found essential:

  • Text: for notes, drill descriptions, or coach comments.
  • Numeric: for distance, time, stroke rate, splits.
  • Enumeration: for stroke type (free, back, breast, fly), effort level (easy, moderate, hard), or interval type.
  • Person: for the swimmer, the coach, the lane mate.
  • Attachment: for PDFs of a workout sheet or a video of a stroke.
  • Computed: for total volume, average pace, or predicted time.

This unified approach pays off. The same attribute can appear in a quick-log form, a detailed edit view, and a weekly summary screen. The API, import/export, and even a future AI assistant all read the same definition.

Scope: Global Attributes vs. Squad-Level Attributes

Some attributes are universal. 'Distance' means the same thing for every squad. But 'pace target' might mean something different for a sprint group versus a distance group. If you create a single global 'pace target,' you're forcing everyone into the same semantic.

I settled on two layers: organization-level attributes for common concepts, and squad-level attributes for local jargon. A 'threshold pace' attribute might be global, but 'T-pace' as a label might only make sense for the elite group. They shouldn't share an ID unless they mean exactly the same thing.

How do you decide? Ask: can the values be compared side-by-side in a report? If one squad's 'effort' is RPE (rating of perceived exertion) and another's is a percentage of max heart rate, they're different attributes, even if they're both called 'effort.'

Layouts: The Right Fields at the Right Moment

You don't need every attribute on every screen. When a swimmer logs a set during practice, they want speed. When a coach reviews the workout later, they want context. So I designed five layout types:

  • Quick Log: minimal fields—distance, stroke, time, and a comment. Designed for 30 seconds between intervals.
  • Detailed View: shows everything—splits, stroke rate, heart rate, notes, coach feedback.
  • Edit Form: for adjustments, like when the coach changes the main set mid-practice.
  • Workflow Step: the specific fields required to transition from 'planned' to 'completed,' like confirming your split times.
  • List View: for scanning a week of workouts—just key attributes and totals.

Here's a rule that saved me from a lot of pain: attribute configuration defines the data rules; layout defines the presentation; workflow defines the entry conditions. 'Title is required' is an attribute rule. 'You must enter split times before marking a set complete' is a workflow rule. 'Where split times appear on the screen' is a layout rule.

And never rely on the frontend to enforce required fields. Batch imports, API calls, and automated syncs can bypass the UI. The server must validate everything.

Workflows: States and Transitions, Not Just Statuses

A status is a snapshot. A transition is an action. 'Completed' is a status. 'Log split times and mark complete' is a transition. That transition might require a specific role (the swimmer), a specific form (split time entry), and a post-action (update the season total).

When I first built a workout tracker, I let swimmers change status freely. Chaos. A swimmer marked a set 'completed' without logging splits, and the weekly volume report was garbage. The fix was to define transitions: from 'planned' to 'in progress' requires starting the clock; from 'in progress' to 'completed' requires split times and a perceived effort rating.

Multiple workflows per type? Yes, but clarify what that means. You can have a 'short course' workflow and a 'long course' workflow for the same set type. The system picks one based on the pool length. That's conditional routing, and it must be explicit. Otherwise, you'll have a set that exists in two workflows at once, and no one knows which one applies.

Relationships: From Parent Sets to Dependencies

A workout isn't a flat list. A main set might have a warm-up set and a cool-down set as children. A drill might be a prerequisite for a specific set. These are relationships, and they need rules.

I distinguish between two kinds: hierarchical (parent-child for decomposition and aggregation) and associative (named connections like 'depends on' or 'blocks'). A parent-child relationship isn't just a parent_id. It defines which types can be parents, whether a set can have multiple parents, and how volume aggregates up. An associative relationship has direction and constraints. 'Warm-up before main set' is a directed dependency that affects scheduling.

In the attribute model, relationships appear as reference attributes. But the validity of those references is checked by the relationship model, not the attribute model. That keeps the semantics clean.

Configuration Scope: From Project to Space

Here's where I had to unlearn a habit. In many tools, you have 'projects' and within them 'fields.' But a swim team isn't a project with an end date. It's a long-running space that holds seasons, squads, and thousands of workouts. So I renamed 'project' to 'space.' A space is the container for a team's history.

Within a space, you have seasons, which are like delivery contexts. A season has a start and end, but the space lives on. This avoids the trap of creating a new 'project' every season and losing all the historical context.

The configuration scope then has three layers: organization standards, space-level configurations, and delivery context (season or meet). A season can filter, route, and report, but it shouldn't spawn a whole new set of fields. Otherwise, you'll end up with duplicate attributes and split workflows.

At runtime, the system resolves the effective configuration: organization standard, then space overrides, then season-specific rules, then user permissions. And it should be able to explain why a certain field is required or why a certain workflow is active. Without that explainability, you'll be debugging configuration issues at 2 AM.

Versioning: Don't Break History

Changing a workflow or an attribute can break existing workouts. If I add a 'stroke rate' field as required, old workouts suddenly become invalid. If I delete a 'drill' status, past drills lose their place in the workflow.

So I implemented configuration versioning. Low-risk changes—adding an optional field, updating help text—can go live immediately. High-risk changes—deleting a status, changing a field type, altering a relationship rule—create a new version. Existing workouts stay on the old version until you explicitly migrate them.

This is especially important for long-term data. A swimmer's entire career might be in your system. You can't let a configuration tweak erase their history.

Bringing It Together: The Anatomy of a Workout

So what does a properly modeled workout look like?

First, the type: 'Set' defines the object. It has a stable ID, a name, an icon, and a lifecycle.

Second, the attributes: 'distance' (numeric, meters), 'stroke' (enum), 'pace' (computed), 'effort' (enum), 'notes' (text), 'coach comment' (text), 'split times' (list of numeric). These are defined once and reused everywhere.

Third, the layout: quick log for practice, detailed view for review, edit form for adjustments.

Fourth, the workflow: planned → in progress → completed, with a 'voided' state for cancellations. Transitions require specific inputs and roles.

Fifth, relationships: a set belongs to a workout, a workout belongs to a practice, a practice belongs to a season. A set can depend on a drill.

Finally, the scope: the junior squad uses a simplified workflow with fewer required fields; the elite squad has additional attributes like stroke rate and heart rate.

When all these layers work together, you get a system that can answer any question: 'What's the total volume this week?' 'Why is this set marked incomplete?' 'Who changed the pace target?' It's not just a database with a pretty frontend. It's a platform that respects the complexity of swim training.

In the next article, I'll dive deeper into relationships—how to model dependencies, blocks, and cross-season tracking. Because in swimming, as in software, everything is connected.

Share this article:

Comments (0)

No comments yet. Be the first to comment!