Ticket Booking System
A role-based desktop ticket-booking app I built solo in C# / .NET on top of MySQL, set up so the form code never reaches the database directly and adding a new role doesn't ripple through the codebase.
What this is
A desktop app that runs a small ticketing business end-to-end. Organisers create events, customers book tickets, admins vet venues and oversee both. It’s intentionally a locally-installed tool rather than a web app, so there’s no live URL. A walkthrough video lands here when I record one so the role flows are visible without setting up the database yourself.
How I designed it
The application is split into three layers: the screens only handle UI events, the middle layer owns the business rules, and the data layer owns the SQL. The screens never hold raw SQL, and the data layer never holds UI references, so changing either side doesn’t drag the other into a refactor. That separation was the most useful decision in the project.
Each role is its own type with the methods that role is allowed to perform, all sharing the same base. The login screen picks the right one and works with whichever role came back. Adding a fourth role is a new type and one switch entry; nothing else moves. Cross-cutting state (the database connection, the current session, the user-factory) lives in one place rather than scattered across the screens. The first draft had two helpers and every screen ended up importing both, so I collapsed them once the duplication was obvious. The lesson there wasn’t about polymorphism, it was about noticing duplication early enough to clean it up before it becomes structural.
Security sits on the data layer: every query is parameterised, login errors don’t say which of “email or password” was wrong, passwords are hashed at write time, and deleting a user flips a flag rather than dropping the row so the booking history doesn’t lose its foreign keys. The data layer only ever selects the columns the screen actually needs, so password hashes and internal identifiers can’t slip into a form binding by accident.
Mid-build decisions worth flagging
- Venue trust model. Reworked venue creation from organiser-self-serve to admin-vetted, matching how Eventbrite and Skiddle gate venues. Organiser-self-serve was readable in code but wrong for trust.
- Schema gap caught in production. An event-duration field was missing until two events booked the same venue a minute apart. Added the field and a server-side overlap check in the same commit.
- N+1 in the filter hot path. Available-ticket queries fired per row on every filter change. Folded into a single grouped query; the booking list stopped thrashing the database.
Testing
Unit tests on the business-rule methods, integration tests on the screen-to-rules-to-database path, and a system test against the end-to-end role flow: organiser creates an event, admin approves it, customer books, customer cancels, booking moves into the history view. Two non-technical users walked through the app to validate the navigation, and that walkthrough caught two UX gaps the unit tests had no opinion on. I keep thinking about that. A unit test can pin a calculation, but it can’t tell you the user opened the wrong screen first.
Stack
C#, .NET WinForms, MySQL. BCrypt for password hashing. Visual Studio.