Build a Production-Style SQL Server Backend: A Capstone Ticket System Project
This combines every chapter of the advanced track into one realistic deliverable: the backend for TicketDesk, a small support-ticket system, built the way a real backend actually gets built — ambiguous edges, several defensible designs, and a requirement to justify your choices, not just produce code that runs.
Schema Requirements
- Agent: agent_id, name, email (unique), is_active
- Customer: customer_id, name, email (unique)
- Ticket: ticket_id, customer_id (FK), assigned_agent_id (FK, nullable — unassigned tickets are a valid state), status, priority, created_at, resolved_at
- TicketComment: comment_id, ticket_id (FK), author_type, body, created_at
- TicketAudit: populated automatically by a trigger — old_status, new_status, changed_at
The Architecture, Visualized
Business Logic Requirements — Mapped to Where You Learned Each One
| Requirement | Chapter it draws on |
|---|---|
fn_GetOpenTicketCount(@agentId) — scalar or inline TVF, with a justification comment for which type you chose and why |
Ch.2 |
usp_Ticket_Create — TRY/CATCH + transaction, OUTPUT parameter for the new ticket_id |
Ch.1, Ch.4 |
usp_Ticket_Assign — THROWs if the agent is not active |
Ch.4 |
usp_Ticket_Resolve — THROWs if the ticket is already closed |
Ch.4, Ch.5 |
| AFTER UPDATE trigger on Ticket — logs every status change to TicketAudit, correctly set-based for multi-row updates | Ch.7 |
vw_AgentWorkload — one row per active agent, including agents with zero open tickets (mind the JOIN type) |
Ch.6 |
-- A skeleton for one requirement, deliberately incomplete — you decide the JOIN type
CREATE VIEW dbo.vw_AgentWorkload AS
SELECT a.agent_id, a.name, COUNT(t.ticket_id) AS open_ticket_count
FROM dbo.Agent a
-- ??? JOIN dbo.Ticket t ON t.assigned_agent_id = a.agent_id AND t.status IN ('open','in_progress')
WHERE a.is_active = 1
GROUP BY a.agent_id, a.name;
The blank above is deliberate: pick the wrong JOIN type here and agents with zero open tickets silently vanish from the report — the exact LEFT JOIN + WHERE-vs-ON distinction from the Fundamentals course, now applied inside a view that a real dashboard would depend on.
Performance & Security Requirements
- Populate Ticket with 5,000+ rows and design a covering/filtered index for “open tickets by agent, ordered by priority” — prove it with before/after
STATISTICS IO(Ch.8) - Create a least-privilege service account for the application — not db_owner, with explicit GRANTs you can justify one by one (Ch.10)
Self-Check Before You Consider It Done
| Check | Why it matters |
|---|---|
| Run a multi-row UPDATE against Ticket’s status column and confirm every changed row appears in TicketAudit | Catches the single-row-assumption trigger bug from Ch.7 |
| Call usp_Ticket_Assign against an inactive agent | Confirms your THROW logic actually fires, not just compiles |
| Query vw_AgentWorkload and confirm an agent with zero tickets still appears, with count 0 | Confirms the correct JOIN type from the skeleton above |
| Log in as your least-privilege service account and confirm it genuinely cannot do more than granted | The only real proof least-privilege was actually applied, not just declared |
Why This Is the Right Capstone
Every chapter of this track shows up here: functions, procedures with proper error handling, a correctly set-based trigger, a view with the right JOIN type, indexing backed by real measurement, and least-privilege security. It mirrors how a real backend ticket actually gets built — ambiguous edges, multiple valid designs, and a requirement to justify your decisions, not just produce working code.
Enjoyed this?
Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.
📡 Subscribe via RSS |
Share on X |
Share on LinkedIn |
Share on Facebook
You’ve completed the full curriculum! Check out SQL Server Fundamentals and SQL Server for Developers & DBAs, coming soon as structured courses on this site.