← All projects

CredosisApi - Meeting Booking & Business Operations Backend

A production .NET backend powering public meeting scheduling and an admin operations console: slot generation, concurrency-safe booking, Google Calendar + Meet sync, and reliable transactional email.

Role
Backend Engineer / Architect
Timeframe
2025
.NET 8 ASP.NET Core EF Core PostgreSQL Google Calendar API Brevo Docker Render Serilog JWT

Overview

CredosisApi is the backend that runs Credosis’s public-facing meeting scheduler and the internal operations console behind it. Visitors browse open time slots and book a meeting in a couple of clicks — similar to Calendly — and the system takes care of everything that has to happen behind that click: reserving the seat safely under concurrent load, creating a Google Calendar event with a Google Meet link, and sending branded confirmation emails to both the visitor and the team.

On the other side, an authenticated admin dashboard lets the team define weekly availability, generate concrete bookable slots, manage bookings (reschedule and cancel), triage contact-form submissions, watch email deliverability, and read a full audit trail of every privileged action.

The goal was a small, dependable service with no moving parts that surprise you in production — where a third-party outage degrades gracefully instead of taking down a booking, and where every state change is observable after the fact.

The Problem

Credosis needed a self-hosted booking backend rather than a SaaS scheduler, for three reasons:

Layered on top of scheduling, the business also needed contact management, analytics, and an audit trail — the operational plumbing that turns a booking endpoint into a usable internal tool.

Architecture

The service follows a layered architecture with strict dependencies pointing inward, which keeps business rules independent of framework and infrastructure concerns.

Presentation  →  Controllers, request/response DTOs, validation, exception handling
Business      →  Services (use cases), domain exceptions, interfaces
Data          →  EF Core DbContext, entities, configurations, repositories, migrations
Infrastructure→  Google Calendar, email (Brevo/SMTP), options, background workers
Common        →  Cross-cutting: time zone clock, pagination, rate-limit policies

A few conventions hold the codebase together:

Key Modules & Solutions

Meeting booking system

The scheduling core has three layers of its own: availability rulesgenerated slotsbookings.

Admins define weekly availability rules (day of week, start/end time, slot duration, capacity). A slot generator then materializes those rules into concrete, bookable MeetingSlot rows for a requested date range. Keeping generation explicit — an admin action rather than a side effect of a public read — means the public GET endpoints stay side-effect free and slots never silently reappear after cleanup.

// Read-only public slot listing: slots are created explicitly via the admin
// "generate" endpoint, never as a side effect of a public read.
var effectiveStart = request.Start < now ? now : request.Start;
var slots = await _slotRepository.GetAvailableAsync(
    effectiveStart, request.End, cancellationToken);

return slots
    .Where(x => x.AvailableSpots > 0)
    .Select(/* map to response */)
    .ToList();

The booking lifecycle covers book → reschedule → cancel, and each transition keeps the slot’s seat count, the calendar event, and the notification emails in sync.

Concurrency-safe booking

The most interesting correctness problem here is the classic one: two people booking the last seat at the same time. The system solves it with an optimistic concurrency token plus a database transaction.

await using var transaction = await _context.Database
    .BeginTransactionAsync(cancellationToken);

var slot = await _context.MeetingSlots
    .FirstAsync(x => x.Id == request.MeetingSlotId && x.IsActive, ct);

if (slot.AvailableSpots <= 0)
    throw new InvalidOperationException("This meeting slot is fully booked.");

slot.BookedCount++;
slot.Version++;   // optimistic concurrency token
// ... create the booking, then SaveChanges + Commit

If a competing transaction commits first, SaveChanges throws a DbUpdateConcurrencyException, which is caught and turned into a friendly “try another slot” message rather than a 500. A unique constraint also prevents the same visitor from double-booking the same slot, and that violation is translated into its own clear error.

The result: under a race, exactly one booking wins, the loser gets an actionable message, and the seat count never goes negative.

Google Calendar & Meet integration

Every booking creates a Google Calendar event with an auto-generated Meet link, the visitor added as an attendee. The integration authenticates with the OAuth2 installed-app refresh-token flow, so the service holds a long-lived refresh token and transparently mints access tokens as they expire — no interactive login in production.

The defining design decision is that the whole integration is best-effort:

public async Task<CalendarEventResult> CreateEventAsync(/* ... */)
{
    if (!IsEnabled) return default;         // inert no-op when unconfigured

    try
    {
        // ... build event with a Meet conference request, insert it
        return new CalendarEventResult(created.Id, meetingUrl);
    }
    catch (Exception ex)
    {
        // Never let a calendar failure break the booking.
        _logger.LogError(ex, "Failed to create Calendar event; booking continues.");
        return default;
    }
}

A Google outage, a network blip, or missing configuration can never fail a visitor’s booking — worst case, the booking is saved without a Meet link. On reschedule the event is patched (preserving the Meet link and attendees); on cancel it’s deleted. Crucially, those calendar calls happen after the database transaction commits, so a slow Google response never holds a DB lock or blocks the user’s request path.

Email & the transactional outbox

Email reliability is handled with the transactional outbox pattern. Services never call the email provider inline. Instead they enqueue a message, which is persisted to an OutboxMessage table in the same unit of work as the booking. A background OutboxProcessor then picks up pending messages and delivers them with retry and backoff.

This buys two guarantees:

Delivery goes through the Brevo HTTP API (port 443) rather than SMTP, because many hosts — including Render’s free tier — block outbound SMTP ports. A full SmtpEmailSender remains in the codebase as a drop-in fallback transport. Every message type has a branded HTML template built on a shared layout: booking confirmation, reschedule, cancellation, and the matching admin notifications, plus a contact-form notification and admin-composed one-off emails with attachments.

Contact management

Contact-form submissions are captured as Contact entities with a status lifecycle (new → contacted → resolved), an admin notification email on arrival, and paginated, filterable admin endpoints for triage. It’s deliberately simple, but it rounds out the “operations console” story so the team doesn’t need a second tool for inbound leads.

Audit logging

Every privileged action — creating an availability rule, generating slots, cancelling or rescheduling a booking, changing a contact’s status — is written to an append-only audit log via an IAuditLogger, which uses the HTTP context to capture the acting admin and client IP. A query service exposes the trail with pagination and filtering, so “who changed this, and when” is always answerable.

Dashboard, analytics & auth

The admin surface is protected by JWT bearer authentication, with options validated on startup so a misconfiguration fails fast rather than at first request. A dashboard service aggregates operational metrics (bookings, contacts, email health) and an analytics service exposes trends, giving the team a single at-a-glance view.

Challenges & Solutions

Time zones. Availability rules are written in Bangladesh wall-clock time, but slots are stored as UTC instants. Early on, “09:00” was being stored as 09:00 UTC and shown as 3 PM locally. The fix was a dedicated BusinessClock abstraction: rule times are interpreted in the business time zone and converted to UTC at generation time, and Google Calendar events are handed the wall-clock time plus an IANA zone id so Google stores the correct instant.

Slot lifecycle after rule edits. Editing or deleting an availability rule shouldn’t strand mismatched slots or silently drop confirmed bookings. The generator distinguishes cases carefully: future unbooked slots from a changed rule are removed, future booked slots are deactivated (bookings preserved, no new bookings accepted), and past slots are always kept as history.

Third-party failure isolation. Both Google Calendar and email are treated as untrusted collaborators. Calendar calls are best-effort and run after commit; email goes through the durable outbox. Neither can take down the core booking flow.

Consistent errors. Domain exceptions mapped to Problem Details responses give the frontend a predictable error contract, so the UI can distinguish “slot full” (conflict) from “not found” from “validation failed” without string-matching messages.

Deployment

The service ships as a Docker image and runs on Render, configured through render.yaml. Configuration and secrets come from environment variables (with a gitignored .env for local development), and connection strings accept either native Npgsql key-value form or a postgres:// URI, so a provider-issued string can be pasted in as-is. Health checks (including a database readiness probe) back the platform’s health monitoring, and database migrations can be applied automatically on startup behind a config flag. Serilog provides structured request and application logging, with X-Forwarded-* headers honored so the real client IP and scheme survive the reverse proxy.

Takeaways