Category: Solutions

  • Migrating Our Monolith to Microservices on GCP

    As the tech lead on this project, I oversaw the transformation of our legacy monolithic application—handling everything from user accounts and digital card sharing to lead capture and CRM integration—into a suite of independent microservices running on Google Cloud Platform. In this article, I’ll share both the theoretical underpinnings and the hands-on steps we took to achieve a scalable, resilient, and fast-release architecture.

    1. Why We Needed to Change

    When we hit our stride at large industry events, our single-instance deployment struggled:

    • Traffic Spikes: QR scans and lead submissions would jump 5×, causing timeouts and memory pressure.
    • Slow Releases: Even minor updates took 30–45 minutes to deploy, locking our teams out of rapid iteration.
    • Cascading Failures: A bug in our CRM sync logic would sometimes stall login requests, degrading the entire user experience.

    We needed a way to scale features independently, reduce blast radius of failures, and accelerate our delivery pipeline.

    2. Core Principles That Guided Us

    Before writing a single line of new code, we aligned on key microservices concepts:

    1. Bounded Contexts: We drew clear boundaries—Auth, Card Sharing, Lead Processing, CRM Sync, and Analytics—so each team owned its domain and data.
    2. Strangler Fig: We planned to incrementally replace monolith endpoints, routing a fraction of real traffic to new services, then steadily increasing until the old code could be retired.
    3. Single Responsibility: Every service would do one thing well, reducing complexity and making testing and deployment straightforward.
    4. Decentralized Data: Moving from one PostgreSQL instance with 30 tables to multiple Cloud SQL instances kept schemas focused and migrations safer.
    5. Infrastructure as Code: We defined our GKE deployments, Helm charts, and API Gateway configs declaratively to maintain consistency across environments.

    3. Our Starting Point: The Monolith

    I often remind the team how our stack looked in production before migration:

    LayerTechRole
    Front-endReact 18 + ReduxNext.js SSR for landing pages
    API & LogicNode.js 16 + Express.js~20 000 LOC, JWT auth
    DatabasePostgreSQL (Cloud SQL)Shared schema, complex migrations
    Job QueueRedis + BullBackground jobs for emails & retries

    This tight coupling meant one change could ripple everywhere.

    4. Mapping Out Bounded Contexts

    We held a workshop to carve out our domains:

    ServiceOwned DataResponsibilities
    Authusers, rolesSignup, login, JWT generation
    Cardcards, sharesQR/NFC generation, share logging
    Leadleads, eventsConsuming share events, data enrichment
    CRM Syncsync_jobsDispatching and retrying webhooks
    AnalyticsmetricsAggregating usage data, dashboards

    Each service would communicate via Pub/Sub topics—CardShared, LeadCaptured, etc.—enabling asynchronous, reliable workflows.

    5. Our Migration Approach

    Here’s how we systematically strangled the monolith:

    1. Set Up GCP API Gateway
      • Configured a single edge entry point; enforced JWT validation before traffic hit our services.
    2. Extract Auth Service
      • Spun up an Express.js/TypeScript repo.
      • Migrated /signup, /login, /profile endpoints.
      • Deployed on GKE under /auth/* and toggled monolith redirects.
    3. Extract Card Service
      • Ported QR/NFC logic into its own Node.js service.
      • Published CardShared events to Pub/Sub.
      • Ran 10% of share traffic through the new service, then ramped up.
    4. Build Lead Service
      • Subscribed to CardShared, enriched lead data, wrote to its own Cloud SQL.
      • Ensured idempotency using unique event IDs.
    5. Launch CRM Sync Service
      • Created a microservice for webhook dispatch with Redis + Bull for retry and dead-letter queues.
      • Repointed all CRM calls from the monolith to this service.
    6. (Optional) Analytics Service
      • Later, we isolated reporting to a Python service reading from its own analytics database.

    By the end, the monolith served only fallback traffic until we fully decommissioned each module.

    6. Infrastructure and Deployment

    Our stack on GCP looked like this:

    • API Gateway handling routing, JWT auth, and rate limits at the edge.
    • Istio Service Mesh enforcing mTLS, circuit breaking, and capturing telemetry to Cloud Monitoring.
    • Cloud Build pipelines with automated testing, image builds, and Helm deployments.
    • Observability via Prometheus, Grafana, and Jaeger to trace cross-service calls.

    7. Testing Strategy

    To ensure quality at each stage:

    • Contract Tests: We used Pact to guarantee new services met monolith expectations before cutover.
    • Integration Tests: Jest and supertest for HTTP endpoints; isolated Cloud SQL instances in Docker for CI.
    • End-to-End Smoke Tests: Playwright against our staging cluster to validate critical flows.
    • Load Testing: k6 scripts simulating thousands of share events per second to tune autoscaling.

    Each build ran tests automatically, preventing regressions.

    8. Lessons Learned

    What Worked:

    • Incremental migration minimized risk and allowed early wins.
    • Pub/Sub decoupling made it easy to add new consumers (analytics, alerting).
    • Service mesh policies improved security and reliability without code changes.

    Challenges:

    • Ensuring data consistency required careful versioning of our event schemas.
    • Team coordination across multiple repos demanded robust CI/CD governance.

    Conclusion

    Migrating this platform was a journey of balancing theory with practical constraints. If you’re about to embark on a similar path, remember: start small, iterate quickly, and always keep your teams aligned on the end goal.

  • A Layered Testing Strategy for SME Loan Platform

    While this Loan App system dates back to 2018—built on Laravel 5.x and a React 16 (without Hooks) front-end, it had a structured, layered test suite. In 2018, the popular choices included ESLint and JSCS for linting, PHPUnit for PHP, Jest + Enzyme for React, and Cypress or Selenium for end-to-end. Below, we’ll map out five layers of testing with tools available at the time.

    1. Static Analysis (ESLint, JSCS, PHP_CodeSniffer, PHPMD)
    2. Unit Tests (PHPUnit, Jest + Enzyme)
    3. Integration Tests (Laravel HTTP Tests, nock or fetch-mock)
    4. Component Tests (Jest + Enzyme, React Test Utilities)
    5. End-to-End Tests (Cypress or Selenium WebDriver)

    1. Static Analysis: Linting & Style Checking

    Tools: ESLint (with ES6/React plugins), JSCS, Prettier (early adoption), PHP_CodeSniffer, PHPMD

    Why?

    • Enforce consistent style and catch syntax errors before runtime
    • Identify code smells in PHP (unused vars, complexity) via PHPMD
    • Automate formatting with Prettier or JSCS

    Front‑end Example (.eslintrc.js):

    Back‑end Example (.phpcs.xml):

    Run these tools locally and integrate into CI to block bad commits.

    2. Unit Tests: Isolate Core Logic

    Tools: PHPUnit 6.x (Laravel built‑in), Jest 22.x, Enzyme 3.x

    PHPUnit Example (InterestCalculator):

    Jest + Enzyme Example (DocumentTypeUtil):

    3. Integration Tests: API & Data Layer

    Tools: Laravel HTTP Tests (built‑in), nock (for Node), fetch‑mock or jest‑fetch‑mock

    Laravel Example (LoanRequestController):

    React Integration (fetch-mock):

    4. Component Tests: React UI Assertions

    Tools: Jest + Enzyme (shallow, mount)

    Example (LoanStatusCard):

    Mock contexts or Redux stores as needed to supply props and state.

    5. End‑to‑End Tests: Full User Journeys

    Tools: Cypress 3.x (released mid‑2018), Selenium WebDriver with Mocha or PHPUnit

    Cypress Example:

    Run E2E tests on CI or locally—Cypress offers a fast, reliable runner in your browser.

    Conclusion

    A layered approach, linting, unit, integration, component, and E2E, ensures that each part of Laravel + React application remained reliable as we continued to enhance and maintain it.

  • Securing JWTs with HTTP-Only Cookies in E-Commerce Platform

    Imagine your E-Commerce platform uses a Next.js/WordPress monolith that seamlessly routes users to two React SPAs (seller and customer) via JWTs and OAuth 2.0. To harden this flow against XSS and CSRF, we have to shift all tokens into HTTP-only cookies, implement rotating refresh tokens, and provide a robust logout/expiry strategy. You’ll have a single, secure session across all three applications by the end.

    Token Flow Overview

    1. Browser → /api/login: credentials submitted
    2. /api/login → Browser: sets access_token & refresh_token cookies
    3. Browser → /api/graphql: requests include cookies
    4. /api/graphql → Browser: returns user context
    5. Role-Based Redirects: Next.js sends user to Seller or Customer SPA

    Prerequisites

    • Node.js ≥14
    • A Next.js project with WordPress authentication (OAuth 2.0)
    • React SPAs for Seller and Customer, served under the same domain
    • A token store (DB or in-memory cache) for refresh tokens

    1. Issue Access & Refresh Tokens on Login

    In pages/api/login.js, validate users via WordPress, then sign and set two cookies:

    This issues a short-lived access_token cookie and a longer-lived refresh_token cookie, both inaccessible to JavaScript.

    2. Verify Tokens in GraphQL Middleware

    In pages/api/graphql.js, parse cookies, verify tokens, and inject context.user:

    Your resolvers can now guard data based on context.user.role for both SPAs and Next.js pages.

    3. React SPAs: Unified Auth Context

    In both seller and customer apps, wrap your app in AuthProvider:

    Use credentials: 'include' everywhere so cookies travel automatically.

    4. Refresh Tokens: Rotate & Retry

    Refresh Endpoint (pages/api/refresh.js)

    Client Retry Logic (src/fetchWithRefresh.js)

    This ensures seamless background rotation and automatic retry of failed requests.

    5. Logout & Expiry Flow

    Logout Endpoint (pages/api/logout.js)

    Client Logout

    Use this on your logout buttons to clear cookies and revoke server-side tokens.

    Conclusion

    By consolidating access and refresh tokens into HTTP-only cookies, rotating on each use, and providing a clear logout strategy, the E-Commerce platform will benefit from:

    • Strong XSS Protection: JavaScript can’t access your tokens
    • CSRF Defense: leveraging SameSite and optional double-submit tokens
    • Seamless UX: single sign-on across Next.js and both React SPAs
    • Robust Session Management: short-lived access, long-lived refresh, and forced logout flows