DEV Community

Suman Binnala
Suman Binnala

Posted on

How Three Developers Built One Monolith Without Git Merge Hell

Over four intense days from Saturday, September 26 to Tuesday, September 29, the biggest bottleneck wasn't typing speed—it was integration paralysis on deadline day. Here is how we architected ZenZone so three teammates could build in parallel.


The most common way hackathon teams fail isn't running out of ideas; it’s merging when the clock runs down.

Teammate A renames a database column. Teammate B changes an API response shape. Teammate C spends four hours trying to get Webpack to compile Teammate A's code. By the final stretch, everyone is stuck in merge conflicts, the database volume has to be wiped, and the frontend is rendering blank white screens.

When our three-person team kicked off building ZenZone on Saturday, September 26, we made an explicit rule on day one: We are not building microservices to escape coordination, and we are not touching each other's files until integration.

Instead, we designed a monolithic architecture specifically partitioned so three people could work in parallel without blocking each other. Here is the blueprint of how we divided the work, the integration collision we had to solve, and the rules that kept us sane through our final submission on Tuesday, September 29.


1. The Division of Responsibilities

Rather than splitting work by feature ("you build teams, I’ll build judging"), which causes people to touch the database, backend, and frontend simultaneously, we split by architectural domain:

┌─────────────────────────────────────────────────────────────┐
│ PERSON 1: Core Infra, Auth & Normalization                  │
│ • Docker orchestration & Postgres 16 baseline               │
│ • Flyway V1 (Users/Roles) & V2 (Audit Logs)                 │
│ • Spring Security JWT stateless filter                      │
│ • Z-Score normalization math engine                         │
└──────────────────────────────┬──────────────────────────────┘
                               │
┌──────────────────────────────┼──────────────────────────────┐
│ PERSON 2: Submissions, Gallery & Judging                    │
│ • Flyway V3 (Events/Teams), V4 (Submissions), V5 (Rubrics)  │
│ • Submission lifecycle & pre-deadline versioning            │
│ • Judge assignment queue & Conflict of Interest (COI)       │
│ • CSV export service                                        │
└──────────────────────────────┼──────────────────────────────┘
                               │
┌──────────────────────────────┴──────────────────────────────┐
│ PERSON 3: Zero-Build Frontend SPA & Routing                 │
│ • Nginx Alpine static container (Port 3000)                 │
│ • Vanilla ES6 Single-Page Application (Zero npm build step) │
│ • Client API wrapper & state store (authStore.js)           │
│ • Role-based view guards & responsive layout                │
└─────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

This separation gave each person a clean, isolated boundary:

  • Person 1 set up the database and core security framework.
  • Person 2 built the core hackathon business logic.
  • Person 3 built the user-facing interface.

2. The Contract-First Buffer (docs/API-CONTRACT.md)

How does Person 3 build a frontend on Sunday morning when Person 2 hasn't finished implementing the judging endpoints?

Before writing a single line of backend logic, Person 1 and Person 2 wrote docs/API-CONTRACT.md. It defined the exact HTTP method, route, request payload, and response envelope for every endpoint in the platform.

Crucially, we agreed on a universal response envelope:

{
  "success": true,
  "data": { ... },
  "message": null
}
Enter fullscreen mode Exit fullscreen mode

Because Person 3 had the exact contract from day one, the frontend API client (frontend/src/api/client.js) was written with transparent unwrapping:

const json = await response.json();
if (json && json.success !== undefined && json.data !== undefined) {
  return json.data;
}
return json;
Enter fullscreen mode Exit fullscreen mode

Person 3 was able to build the entire Judging View, Gallery, and Submission forms without waiting for Person 2's backend services to be deployed.


3. Numbered Database Migration Ownership

The fastest way to destroy a relational database during a hackathon is having two developers create conflicting migrations. If both create V3__something.sql with different tables, Flyway crashes on boot and checksums fail.

In our recorded implementation_plan.md, we established strict migration version partitioning:

  • Person 1 owned V1__init_schema.sql and V2__audit_log.sql.
  • Person 2 was strictly assigned V3 onwards (V3__events_tracks_teams.sql, V4__submissions_gallery.sql, V5__judging_rubric_coi.sql).

Nobody was allowed to edit an existing migration file once committed. If a schema needed a change, it had to be an additive migration (V6, V7, etc.). Because of this rule, our Docker container ran 13 migrations incrementally from Saturday to Tuesday without a single migration collision or corrupted checksum.


4. The Real Integration Challenge: BIGINT vs. UUID

Even with clean boundaries, integration always produces a surprise. Ours happened on Monday when merging Person 2's submissions and judging schema into Person 1's infrastructure.

As documented in our integration log (walkthrough.md):

"Rather than performing a blind text replacement of Person 2's UUID VARCHAR(36) schema, we adapted Person 2's data models to align with Person 1's relational BIGSERIAL/BIGINT database schema."

Person 2 had modeled IDs as strings (VARCHAR(36)) because the organizer's fixtures.json used string identifiers like prj_01 and evt_01. But Person 1’s users, events, and audit tables were already using PostgreSQL auto-incrementing BIGINT primary keys with foreign key constraints.

If we blindly merged, foreign keys between submissions.created_by and users.id would mismatch on type.

Instead of blowing away Person 1's migrations and restarting the database volume, we solved it architecturally:

  1. We kept BIGINT across all database foreign keys for query speed and relational integrity.
  2. We stored the original fixture strings in an indexed metadata column (content_hash = "fixture:prj_XX").
  3. In DataSeeder.java, we wrote an in-memory translation map (Map<String, Long>) that resolved fixture strings to generated BIGINT IDs during cold boot.

Zero database volumes had to be deleted. All previous authentication tokens and seeded records continued working seamlessly.


5. Decoupling the Frontend: The Zero-Build Bet

Person 3's frontend architecture was our biggest time-saver across the four days. By choosing Vanilla ES6 modules served by Nginx Alpine instead of React or Next.js:

  1. No build step: When Person 2 updated a backend route, Person 3 didn't have to wait 2 minutes for a bundle to compile. A browser refresh loaded the raw .js files immediately.
  2. Instant Docker boots: The frontend container image was 25 MB and started in under a second.
  3. No npm dependency hell: None of the three developers spent time troubleshooting Node version mismatches or peer dependency errors on their laptops.

Top comments (0)