Architectural Shift:
MVC vs. Clean Architecture
An evolutionary case study comparing software design patterns. Evolved from a rapid prototype to a fully decoupled clean architecture, demonstrating the core fundamentals of software engineering: separating domain invariants, abstracting database drivers, and building testable business boundaries.
A Closed-Loop Campus Marketplace
UniCycle Marketplace was built to support safer campus trading for textbooks, electronics, and dorm items. Registration requires a .edu student address as an access gate. This case study documents the evolution of a web monolith into a more decoupled, testable system.
The Shift from Feature Delivery to Architectural Discipline
UniCycle Marketplace was refactored across two engineering phases. Rather than just presenting final feature capabilities, this case study details how structural design priorities evolved when applying enterprise patterns to a monolithic codebase.
Engineering Scope: Express 5, EJS templating, and flexible document storage with MongoDB & Mongoose.
Deliver a fully functional campus trading platform designed under the RUP (Rational Unified Process) framework. The priority was validating campus-email access and identity flows.
Velocity-first monolithic implementation. Express routes were coupled directly to database models to establish core CRUD operations, multi-file uploads, and template rendering within a unified codebase.
Engineering Scope: Decoupled layer boundaries, Repository pattern interfaces, and MySQL relational integrity.
Isolate business rules from the execution environment. The goal was to draw strict boundaries so that domain validation and workflows remain independent of third-party libraries.
Decoupled, boundary-first design. Replaced MongoDB with MySQL to enforce referential constraints, and refactored request pipelines to run through Use Cases, DTOs, and testable repository mocks.
MongoDB (v1) and MySQL (v2)
Entities, Use Cases, Interfaces, Adapters
Controllers route requests through Repositories
Flexible JSON Documents vs. Strict Relational Mapping
How database choices impacted referential integrity and schema stability during development.
Dynamic JSON Collections
Stored users, categories, and listings as unstructured documents. Listings nested arrays of images directly inside the document.
To optimize query read performance. By nesting images and categories as JSON directly inside the listing document, the application could retrieve complete product details in a single database round-trip, completely avoiding the query latency and CPU overhead of multi-table SQL joins.
Poor referential safety. As the status workflow expanded (Draft, Pending, Active, Sold), maintaining consistent state between category deletion and user listings required writing manual hooks. Broken references were hard to prevent without database constraints.
Relational Schema & Foreign Keys
Created formal tables with explicit data types, unique keys, and foreign keys mapping category and user relations.
To enforce real referential integrity. If an administrator deletes a category or user account, SQL constraints (`ON DELETE CASCADE`) guarantee that related listings and images are cleaned up.
Total relational safety. Database structures are strictly versioned via migrations (`migrate.js`, `schema.sql`). While it added structure mapping overhead, it completely eliminated silent dangling data references.
Coupled Schema Rules vs. Framework-Free Domain Entities
Isolating system invariants and core logic from third-party database libraries.
Coupled Schema-Level Rules
Defined user validations (email format, password constraints) directly inside the Mongoose model schema definition in `models/User.js`.
Kept data validation and collection structures in one central model, making it easy to reject bad inputs when saving records to MongoDB.
Tight coupling. If I wanted to validate a user registration payload without connecting to a running MongoDB database, Mongoose would crash. Business rules were bound directly to the database library.
Pure Domain Entities & DTOs
Implemented standalone domain classes (`models/User.js`) with zero database dependencies, alongside Data Transfer Objects (DTOs) for boundary sanitization.
To isolate system rules from infrastructure. DTOs sanitize request bodies, while pure domain entities validate system invariants (e.g. checking listing conditions) using standard JavaScript.
Decoupled domain. Domain models can be tested, loaded in command-line scripts, or moved to a different framework without needing database configurations or third-party schema engine connections.
Fat MVC Monolith vs. Decoupled Use Case Orchestration
Evolving request handling from all-in-one scripts into clean, decoupled boundary interfaces.
All-in-One Controller Functions
Controllers imported Mongoose models directly, handling routing logic, authentication checks, schema queries, and view rendering in a single handler.
Provided the fastest path to connect EJS templates with MongoDB collections, reducing boilerplate and execution overhead in the early prototype iteration.
Severe logic duplication. Controllers had to know session formats, specific database queries, and templates. Changing the image upload path meant hunting down modifications across multiple router controllers.
Single-Responsibility Use Cases
Restructured code into layers: Controllers map parameters to DTOs and call Use Cases. Use Cases execute business logic via abstract Repository interfaces.
To ensure database and framework independence. Controllers only translate network requests, use cases orchestrate operations, and repositories handle storage.
High modularity. I successfully wrote repository adapters for both MongoDB and MySQL. Swapping persistence engines required editing just a single environment variable, proving the power of clean architectural boundaries.
Integration Spin-Ups vs. Sub-Millisecond Mock Testing
Restructuring code parameters to test core business logic without network or database dependencies.
Tied to the Database
Testing user authentication or listing creation required connections to a running MongoDB database instance or an in-memory database server.
Since Mongoose models and database connection instances were initialized directly inside controllers, there was no way to isolate logic checks from the database engine.
Slow, fragile tests. If the local database server had configuration issues, or the network Atlas connection timed out, tests failed. Developers ran tests less frequently due to high setup times.
Pure In-Memory Interfaces
Tested Use Cases directly by injecting simple JavaScript mock classes (e.g. `MockUserRepository`) that simulate database returns using local arrays.
Because use cases only interact with abstract Repository interfaces, we can supply any object that satisfies the interface signatures during verification.
Blazing fast unit tests. All business rules (e.g. blocking suspended users from claiming listings, or checking limit counts) are verified in milliseconds with zero database or socket connections.
Evolution of the Directory Architecture
A side-by-side terminal comparison of the codebase layouts before and after drawing architectural layers.
unicycle-marketplace-v1/
├── config/
│ ├── db.js # MongoDB Connection
│ └── multer.js # Upload Middleware
├── controllers/ # Fat Controller Handlers
│ ├── authController.js
│ ├── listingController.js
│ └── profileController.js
├── models/ # Coupled Mongoose Models
│ ├── User.js
│ └── Listing.js
├── routes/ # HTTP Routers calling Controllers
│ ├── authRoutes.js
│ └── listingRoutes.js
├── views/ # UI Layout Templates
│ └── index.ejs
└── app.js # Entry Point & Middlewares
unicycle-marketplace-v2/
├── app.js # Bootstrapping Entry Point
├── package.json # Scripts & Dependencies
├── src/
│ ├── domain/ # Enterprise Business Rules
│ │ ├── users/userRules.js
│ │ └── listings/listingRules.js
│ ├── application/ # Application Use Cases
│ │ ├── auth/authUseCases.js
│ │ └── listings/listingUseCases.js
│ ├── infrastructure/ # Concrete Tech Adapters
│ │ ├── database/mysql/repositories/
│ │ ├── security/bcryptPasswordHasher.js
│ │ └── storage/localListingImageStorage.js
│ ├── interfaces/ # Express Delivery Layer
│ │ ├── web/controllers/
│ │ ├── web/routes/
│ │ └── web/dtos/CategoryDTO.js
│ ├── composition/ # DI Central Container
│ │ └── container.js # Composition Root
│ └── shared/ # Cross-Cutting Utilities
│ ├── errors/ApplicationError.js
│ └── result.js
└── test/ # Use Cases Unit Test Suite
└── application/listingUseCases.test.js
Evolution of Platform Capabilities
Comparing the implementation mechanics of core features between prototyping and clean architecture stages.
F01 User Authentication & Session State
Stack: bcrypt + express-session + Mongoose
Session variables are accessed directly in routes and controllers. User validation rules are declared inline inside Mongoose database schemas.
Simple to implement but tightly coupled. Any modification required updating controllers, templates, and mongoose queries concurrently.
Stack: bcrypt + express-session + UserRepository
Inputs map through UserDTO. Auth middleware references abstract Repository interfaces to isolate user session validation from database schema rules.
Business rules run independently. Use cases manage feature logic, controllers bridge HTTP requests, and repositories translate database operations.
F02 Listing CRUD Operations
Stack: Mongoose + EJS layouts
Controllers execute Mongoose CRUD operations directly, rendering the view and fetching MongoDB models inside Express handlers.
Simple to implement but tightly coupled. Any modification required updating controllers, templates, and mongoose queries concurrently.
Stack: Domain Entities + Use Cases + MySQL Repository
Router maps inputs to ListingDTO. CreateListingUseCase coordinates domain validations and triggers the repository to persist relation changes.
Business rules run independently. Use cases manage feature logic, controllers bridge HTTP requests, and repositories translate database operations.
F03 Upload Review & Image Safety Flow
Stack: Multer + local storage checks
Multer saves files to uploads. Image paths are saved directly in listing documents; admins toggle visibility by modifying document arrays.
Simple to implement but tightly coupled. Any modification required updating controllers, templates, and mongoose queries concurrently.
Stack: Staged Multer Uploads + Binary Signature Validation
Uploads are staged outside the public directory, checked against extension, MIME, and binary signatures, then promoted through storage and listing use-case boundaries.
Business rules run independently. Use cases manage feature logic, controllers bridge HTTP requests, and repositories translate database operations.
F04 Campus Email Access Gate
Stack: Mongoose schema and controller validation
Student email and access rules live beside persistence concerns, leaving registration behavior coupled to the document model and HTTP flow.
Simple to implement but tightly coupled. Any modification required updating controllers, templates, and mongoose queries concurrently.
Stack: Domain Rule + Auth Use Case + UserRepository
The .edu address rule runs before persistence, while account uniqueness, roles, and status data remain behind repository and relational boundaries.
Business rules run independently. Use cases manage feature logic, controllers bridge HTTP requests, and repositories translate database operations.
F05 Notification & Event System
Stack: connect-flash only
State alerts are transient and coupled directly to HTTP redirects; no historical record of admin actions or listing updates is persisted.
Simple to implement but tightly coupled. Any modification required updating controllers, templates, and mongoose queries concurrently.
Stack: Notification Repository + Event Trigger Flow
Persistent event logs generate real-time bell badges and in-app historical ledgers for listing reviews, support updates, and new message alerts.
Business rules run independently. Use cases manage feature logic, controllers bridge HTTP requests, and repositories translate database operations.
F06 Marketplace Messaging
Stack: No dedicated marketplace inbox
The prototype did not provide persistent listing-linked buyer and seller conversation threads inside the application.
Simple to implement but tightly coupled. Any modification required updating controllers, templates, and mongoose queries concurrently.
Stack: Messaging Use Cases + Thread Repository
Students open conversations from a listing, retain message history and read state, and manage listing context through a dedicated split-pane inbox.
Business rules run independently. Use cases manage feature logic, controllers bridge HTTP requests, and repositories translate database operations.
F07 Support Triage & Listing Reports
Stack: One-way static inquiry contact form
Student reports and help inquiries are stored as static entries. No communication threads or listing-linked reports are supported.
Simple to implement but tightly coupled. Any modification required updating controllers, templates, and mongoose queries concurrently.
Stack: Two-way Conversation Threads + Moderation Triage
Students submit reports directly from listings. Support tickets support multi-party replies and status triggers (Resolved/In-Progress).
Business rules run independently. Use cases manage feature logic, controllers bridge HTTP requests, and repositories translate database operations.
F08 Favorites & Bookmarks
Stack: Not implemented
Marketplace listings cannot be bookmarked or saved. Students have to manually search for previously viewed items.
Simple to implement but tightly coupled. Any modification required updating controllers, templates, and mongoose queries concurrently.
Stack: Favorite Listing Join Table + Dashboard Widgets
Users toggle favorites directly from cards or details. Saved items are tracked via active joins and render in a dedicated, high-speed board.
Business rules run independently. Use cases manage feature logic, controllers bridge HTTP requests, and repositories translate database operations.
F09 Listing Routing & Slug Resolution
Stack: Numeric ID routes
Detail pages resolve strictly via database-level integer primary keys, exposing structural counters in URLs (e.g., /listings/13).
Simple to implement but tightly coupled. Any modification required updating controllers, templates, and mongoose queries concurrently.
Stack: Hybrid Title-Slugs with Legacy Redirects
Generates clean, human-readable SEO slugs (e.g., /listings/13-calculus) while transparently parsing numeric IDs and redirecting old formats.
Business rules run independently. Use cases manage feature logic, controllers bridge HTTP requests, and repositories translate database operations.
F10 Design System & Typography Architecture
Stack: Bootstrap 5 defaults + ad-hoc custom styles
Standard Bootstrap utilities and browser default fonts without centralized spacing tokens or consistent hairline border boundaries.
Simple to implement but tightly coupled. Any modification required updating controllers, templates, and mongoose queries concurrently.
Stack: Space Grotesk + DM Sans + Flat Hairline Token System
Dual typography hierarchy (Space Grotesk display headings, DM Sans UI controls, System Mono metadata) paired with 0.5px hairline operational borders.
Business rules run independently. Use cases manage feature logic, controllers bridge HTTP requests, and repositories translate database operations.
Architectural Journal Entries
Key takeaways and architectural insights from refactoring a server-side monolith into an isolated domain model.
Relational Structure Rigor
Migrating to MySQL forced me to design data relationships and cascading constraints upfront. It proved that enforcing data integrity at the database layer prevents bad state errors from ever creeping into application code.
Isolating Business Rules
By keeping the core domain logic free of Express and database driver imports, the system remains protected against third-party dependency updates. The core rules of the marketplace remain stable even if the delivery framework changes.
Engineering as a Craft
“Evolving UniCycle Marketplace from a working MVC prototype to a decoupled, testable Clean Architecture codebase wasn't just a requirement, it was a personal pivot. For me, software development is about more than just shipping features; it is about building systems that are durable, adaptable, and a pleasure to maintain. By applying enterprise boundaries to this codebase, I validated my approach to engineering: treating code not just as a tool, but as a craft.”