This write-up walks through the design and build of a model-driven Dynamics 365 app for booking sports courts — a "Shuttle Court Booking" system for a facility that runs several indoor courts and wanted to get off phone calls and a whiteboard for scheduling. Think of it as a members-only version of booking a table at a restaurant: check what's open, reserve a slot, get a confirmation, and the venue can't accidentally hand your slot to someone else. It's presented here as an illustrative example of how I'd approach a model-driven app from data model through automation and security, not a transcript of any single client engagement — the entity names, field choices, and plugin logic below are the kind of decisions that come up on real booking and scheduling projects, generalized into a self-contained example.
The Problem
Before the app, court booking ran on a mix of phone calls, a paper sign-in sheet at the front desk, and a shared spreadsheet that front-desk staff updated between other tasks. That's manageable when a handful of people are involved, but it's the same reason two people can't reliably share one paper calendar without occasionally double-booking the same slot — and that's exactly what started happening as membership grew: double-bookings occurred whenever two staff members updated the spreadsheet at close to the same time, members couldn't check availability without calling during business hours, and there was no reliable record of who had cancelled late or simply not shown up. The facility wanted a self-service system members could use to check availability and book a court themselves, like booking a restaurant table online, with staff retaining the ability to book on a member's behalf and manage the schedule from the back office, without introducing a separate piece of software the front desk would have to learn from scratch alongside their existing Dynamics 365 CE deployment.
Solution Overview
Because the facility already ran Dynamics 365 CE for membership and front-desk operations, extending it with a model-driven app was a more natural fit than standing up a separate booking product and integrating the two — one system to maintain rather than two systems that have to be kept in sync. A model-driven app gave staff a consistent, form-and-view-based interface for managing courts, time slots, and bookings that looked and behaved like the rest of their existing Dynamics 365 CE tools, while a lightweight, security-role-scoped self-service view gave members a way to see availability and request a booking without needing full back-office access. The core of the build was a small, purpose-built data model, four plugins enforcing the booking rules that actually matter (no double-booking, correct pricing, respected cancellation windows), and three Power Automate flows handling the communication side — confirmations, reminders, and no-show tracking — the kind of "let someone know" logic that doesn't need to happen instantly or block anything, so it didn't need to live in code-level, synchronous logic.
Data Model
The data model stayed intentionally small. Six custom tables covered the whole booking lifecycle, each with a clear, single responsibility:
| Entity | Purpose |
|---|---|
| Court | A bookable physical court — name, sport type, hourly rate, and active/inactive status. |
| Time Slot | A discrete bookable interval for a specific court on a specific date, generated ahead of time from the facility's operating hours. |
| Member | A person with booking privileges — membership tier, contact details, and a rolling no-show count. |
| Booking | Links a Member to one or more Time Slots on a specific Court, with a status (Confirmed, Cancelled, Completed, No-Show) and calculated cost. |
| Payment | Records payment or credit applied against a Booking, including refund handling for cancellations inside a refundable window. |
| Cancellation Request | A record of a cancellation attempt against a Booking, capturing whether it fell inside or outside the allowed cancellation window and why. |
Court and Time Slot are deliberately separate tables, and that split is the single most important decision in the whole data model. A scheduled flow generates the next several weeks of Time Slot records for each court in advance, based on the facility's operating hours, so a Booking always attaches to an existing, pre-defined slot rather than a start and end time someone typed in by hand. It's the difference between choosing a seat from a numbered seating chart and just writing down "somewhere in row C" — one is either taken or it isn't, and the other invites disagreement. That single decision eliminated an entire category of "is this actually a valid bookable time" validation that would otherwise have needed to live inside the booking logic itself.
Automation & Business Logic
Four plugins carried the business rules that had to be enforced no matter which path a booking came through — the member self-service view, staff working the front desk, or a future integration. Plugins were the right tool for these four specifically because each one has to run as part of the save itself, not sometime after — the same way a cashier can't ring up your item and only check afterward whether it was actually in stock:
- Prevent double-booking — the same idea as a venue not letting two people book the same seat for the same show. Registered on the pre-validation stage of Create and Update on Booking, this plugin checks whether the target Time Slot already has an active Booking linked to it and blocks the save with a clear error if so. Running at pre-validation, rather than post-operation, was the deliberate choice here — the whole point is to stop the conflicting booking from ever being written, not to detect and clean it up after the fact.
- Auto-calculate booking cost — a pre-operation plugin on Booking create/update that looks up the linked Court's hourly rate and the Time Slot's duration, applies any active membership-tier discount, and writes the calculated total to the Booking before it saves. This is the same reason a cash register calculates your total instead of the cashier doing mental math — it removes the chance of front-desk staff typing in a price by hand and getting it wrong.
- Cancellation window validation — a pre-validation plugin on the creation of a Cancellation Request that compares the current time against the booking's start time and the facility's configured cancellation window (for example, four hours' notice), marking the request as refund-eligible or not before it's ever presented to staff for approval — much like an airline automatically deciding whether you qualify for a fee-free change based on how close to departure you are.
- Release time slot on cancellation — a post-operation plugin on Booking status changing to Cancelled or No-Show that flips the linked Time Slot back to available, so it immediately shows up as bookable again for someone else. It runs asynchronously since it doesn't need to block the cancellation itself and can comfortably happen a moment after the status change commits.
Power Automate Flows
Three flows handled the communication and monitoring layer that sits naturally outside plugin logic — nothing here needs to happen instantly or block a save, so it made more sense as background automation a non-developer could see and adjust later, rather than compiled code:
- Booking confirmation flow — triggered on Booking create with a Confirmed status, this Dataverse-triggered flow sends a confirmation email to the member, the same kind of "your booking is set" message you'd get after buying a movie ticket online, and for staff-created bookings also posts a Teams notification to the front-desk channel so the team has visibility without checking the app directly.
- Reminder flow — a scheduled flow that runs hourly, checks for upcoming bookings within a configured reminder window (two hours before the slot, in this build), and sends a reminder email or SMS-style notification, the same nudge a dentist's office sends before an appointment, reducing the number of forgotten bookings turning into no-shows.
- No-show tracking flow — triggered when staff mark a Booking as No-Show, this flow increments the Member's rolling no-show count and, once that count crosses a configured threshold within a rolling period, flags the Member record for staff review rather than silently restricting anything automatically — a human still makes the final call, the flow just surfaces the pattern.
Client-Side Validation
On top of the server-side plugin checks, the Booking form included JavaScript form validation to give staff and members immediate feedback before a save was even attempted, rather than waiting for a round trip to the server — the same reason a checkout page tells you a card number looks wrong before you even hit submit, instead of making you wait for the payment to fail. The script checked the selected Time Slot's status as soon as it was chosen, warning immediately if it had become unavailable since the form loaded, and validated that a cancellation being entered fell within a sensible date range relative to the booking. This client-side layer never replaced the plugin-level checks — a user could still bypass JavaScript entirely — but it meaningfully cut down on failed saves and the confusing "why did that get rejected" moments that come from only finding out about a conflict after clicking save.
Security Model
Three security roles covered the people who needed access to the system, each scoped to what that group actually needed to do — the same logic as a gym handing out different keycard levels to members, front-desk staff, and the manager, instead of giving everyone the same master key:
- Member — read access to Court and Time Slot records to check availability, and create/read access to their own Booking and Cancellation Request records at Basic (User) level, so a member could see and manage only their own bookings, the same way an app only shows you your own orders.
- Staff — Business Unit-level read/write on Booking, Member, Payment, and Cancellation Request, letting front-desk staff book on behalf of any member, process cancellations, and record payments, without needing System Administrator-level access to the environment.
- Admin — Organization-level access across all six tables, plus rights to manage Court records and pricing, used by the small group responsible for configuring rates, generating Time Slot records, and reviewing no-show trends.
Keeping the role count at three, rather than growing a separate role for every minor variation in responsibility, kept the security model easy to reason about and easy to explain to the facility's own staff during handover — a consideration that's easy to deprioritize during a build but that matters enormously for who ends up able to actually maintain the system after go-live.
Outcome & Lessons Learned
The finished system replaced the spreadsheet-and-phone-call workflow with a self-service booking experience for members and a single, consistent management view for staff, while eliminating double-bookings by construction rather than by policy — meaning it's not that staff were told to be careful, it's that the system won't let the conflicting booking happen in the first place. The bigger lesson from a project like this is less about any single feature and more about sequencing: getting the Time Slot/Booking split right in the data model up front made almost everything downstream — the plugins, the client-side validation, even the reminder flow's scheduling logic — simpler than it would have been if availability had been modeled as loose date/time fields on the booking itself. Model-driven apps reward that kind of early investment in data model shape, the same way a well-organized filing cabinet makes every later task faster; a clean table design tends to make the automation layer feel almost obvious in comparison, while a data model built around convenience shortcuts tends to make every later piece of logic more complicated than it needs to be.