Answer-first: Three database strategies exist for Magento→Golang migration: (A) Shared DB — immediate compute win, dangerous as a final state; (B) CDC + Debezium — gradual DB separation without changing PHP code; (C) Full Event Bus — complete autonomy but requires Magento to publish every state change as an event. Option B is the recommended path for most teams.

What You’ll Learn That AI Won’t Tell You

  • Why Go running against Magento’s MySQL is faster at the compute layer but still bottlenecked at the EAV query layer — and what actually fixes it.
  • The single deciding factor between CDC (Option B) and Event Bus (Option C): who owns the PHP Magento codebase.

This post is part of the Composable Commerce Migration series — a step-by-step playbook for migrating Magento 2 to Go microservices. For the full migration execution guide, see Part 6: Phase 1 Strangler Fig.

The Strangler Fig Dilemma: Compute vs. Data

When migrating away from a Magento monolith to a Golang backend, architects face a non-obvious decision: do we migrate the API layer and the database at the same time?

Migrating both simultaneously requires setting up Eventual Consistency, Dual-Writes, and Saga patterns from Day 1. This delays time-to-market and prevents the business from seeing performance improvements for 9–18 months.

The alternative is the Shared Database Quick Win — migrate the API compute layer first, keep the data layer intact, and buy time to architect a proper DB separation strategy.

But “buy time” only works if you have a clear plan for Phase 2. This post defines all three options and gives you a decision framework to pick the right path.


Three Options on the Table

┌──────────────────┬──────────────────────────┬──────────────────────────┐
│  OPTION A        │  OPTION B                │  OPTION C                │
│  Shared DB       │  Evolutionary CDC        │  Full Separation +       │
│  (Quick Win)     │  (Debezium + Outbox)     │  Event Bus (Kafka)       │
├──────────────────┼──────────────────────────┼──────────────────────────┤
│  Go reads the    │  Go owns write DB.       │  Fully separate DBs.     │
│  same Magento    │  CDC syncs reads from    │  All comms via Kafka.    │
│  MySQL directly  │  Magento. No PHP changes │  Magento emits events    │
└──────────────────┴──────────────────────────┴──────────────────────────┘

Option A — Shared Database (The Quick Win)

┌──────────────┐         ┌──────────────────────────────┐
│  Go (magento-go) │──READ──▶│                              │
│              │──WRITE─▶│   MySQL Magento (shared)     │
│  PHP Magento │──READ──▶│                              │
│              │──WRITE─▶└──────────────────────────────┘
└──────────────┘

In Phase 1 of a Strangler Fig, you introduce a smart router (e.g., routemode shadow proxy) in front of Magento, then rewrite high-throughput APIs in Go — but connect Go directly to the existing Magento MySQL.

Why It Works (Immediate Gains)

Magento PHP can take 100–200ms just to bootstrap before executing a single SQL query. Go eliminates this overhead entirely. Measured in a real Magento→Go migration (mag-go), the same authentication endpoint dropped from 180ms to 8ms — a 22× reduction — without changing the database query at all.

  1. Compute decoupling is instant. The PHP framework overhead (DI container, ORM hydration, module observers) is gone.
  2. Zero data sync issues. Both systems read the same committed MySQL rows — no eventual consistency lag.
  3. Rollback is trivial. One config change in routemode flips traffic back to PHP.

The Dark Sides — Why This Cannot Be the Final State

Dark Side 1: Magento DB is a “Hidden API” You Don’t Control

Magento’s schema is not your API — but Go is now coupled to it as if it were.

Magento upgrade 2.4.6 → 2.4.7
  → Column renamed in customer_entity
  → Go struct binding silently fails or panics
  → Incident at 2am

Adobe has deprecated the “split database” feature in Magento 2.4.6+. Every upgrade consolidates the schema further. Each Go struct pointing at a Magento table is an undeclared dependency with no SLA.

Dark Side 2: Write Conflict — Race Conditions on Shared Tables

TablePHP writesGo writesRisk
customer_entity✅ (address, token updates)HIGH
oauth_token✅ (magento-go auth module)CRITICAL
customer_address_entityHIGH
quote / sales_orderProxy onlyMedium

MySQL row-level locks prevent dirty reads but do not prevent business logic conflicts. If Go updates a customer session token at the same moment Magento invalidates it, the result is corrupted auth state — not a database error.

Dark Side 3: Distributed Monolith Trap

Go service ───────▶ Shared MySQL ◀─────── PHP Magento
    ↑                                           ↑
"microservice"                       monolithic coupling in practice

You now have two separate runtimes but one failure domain. A Go query that performs a full-scan on catalog_product_entity_varchar degrades Magento’s checkout performance. This is architecturally identical to a monolith — it is just harder to debug because two codebases are involved.

Dark Side 4: EAV Is the Real Bottleneck — Go Doesn’t Fix It

-- Just to load one customer with all EAV attributes:
SELECT ce.*, 
       cevs.value AS first_name,   -- customer_entity_varchar (JOIN 1)
       cevi.value AS store_id,     -- customer_entity_int (JOIN 2)
       ...
FROM customer_entity ce
JOIN customer_entity_varchar cevs ON ce.entity_id = cevs.entity_id
JOIN customer_entity_int cevi ON ce.entity_id = cevi.entity_id
...  -- 5–10 JOINs total

Go executes this query faster than PHP, but the query plan is identical. The bottleneck is the EAV schema design, not the language. The only fix is to flatten EAV into a proper Go-owned schema — which requires DB separation.

How to Execute Option A Safely (Non-Negotiable Constraints)

If you commit to Option A as a transitional state:

  1. Read-only constraint: Go must only SELECT on Magento-owned tables. All writes must be proxied back to Magento’s PHP API.
  2. Table ownership policy: Document which tables Go may read. No undocumented reads allowed.
  3. Schema pinning CI check: Run a schema diff on every Magento upgrade PR. If a column changes that Go references, block the build.
  4. Set a hard deadline for Phase 2: Option A without a Phase 2 date on the calendar will become permanent by inertia.

┌──────────────┐              ┌──────────────────┐
│  PHP Magento │──WRITE──────▶│  MySQL Magento   │
└──────────────┘              │  (master)        │
                              └────────┬─────────┘
                                       │ binlog (no PHP changes needed)
                                       ▼
                               ┌──────────────┐
                               │  Debezium    │  ← reads binlog directly
                               │  CDC Engine  │
                               └──────┬───────┘
                                      │ stream events
                                      ▼
┌──────────────┐  WRITE-OWNED  ┌────────────────────────┐
│  Go (magento-go) │──────────────▶│  Go DB (separate)      │
│              │  READ-SYNCED  │  Flat schema            │
│              │◀──────────────│  token, session,        │
└──────────────┘               │  customer_flat...       │
        │                      └────────────────────────┘
        │ Outbox events (Go → Magento if needed)
        ▼
   ┌──────────┐
   │  Kafka   │──▶ Magento consumer
   └──────────┘

The critical insight: Debezium reads MySQL’s binary log (binlog) directly. This means you never need to modify a single line of PHP Magento code to stream every data change into the Go database.

How CDC + Outbox Works

Step 1 — Go creates its own DB for data it owns:

-- Go-owned tables (Go writes here, Magento never touches)
CREATE TABLE magento_customer_token (
  id UUID PRIMARY KEY,
  customer_id BIGINT NOT NULL,
  token_hash VARCHAR(255) NOT NULL,
  expires_at TIMESTAMP NOT NULL
);

CREATE TABLE magento_outbox (
  id UUID PRIMARY KEY,
  event_type VARCHAR(100) NOT NULL,
  payload JSONB NOT NULL,
  created_at TIMESTAMP DEFAULT now(),
  published_at TIMESTAMP
);

Step 2 — Debezium streams Magento changes → Go DB:

# debezium-connector.yaml
connector.class: io.debezium.connector.mysql.MySqlConnector
database.hostname: magento-mysql-master
database.include.list: magento
table.include.list: magento.customer_entity, magento.catalog_product_entity
# Debezium reads binlog — zero Magento PHP changes required

Step 3 — Go writes use Outbox Pattern (atomic):

// In a single DB transaction — no dual-write risk
tx.Exec(`INSERT INTO magento_customer_token (...) VALUES (...)`)
tx.Exec(`INSERT INTO magento_outbox (event_type, payload)
         VALUES ('TOKEN_CREATED', $1)`, tokenPayload)
// OutboxProcessor publishes to Kafka at 500ms intervals
// Debezium picks up the outbox row → routes to Magento if needed

Why Option B Wins for Most Teams

What you getWhat you avoid
Go owns its schema — flatten EAV → 5× read speedNever touch PHP Magento code
ACID writes on Go’s own DBNo Saga complexity on Day 1
Debezium streams Magento changes to GoNo dual-write race conditions
Per-domain rollout (Auth first, Checkout last)No Big Bang cutover

Option C — Full DB Separation + Event Bus

┌─────────────────────────────────────────────────────────────┐
│                     EVENT BUS (Kafka)                        │
│  topic: customer.updated │ order.created │ inventory.changed │
└──────────┬───────────────────────────────┬──────────────────┘
           │ PUBLISH                       │ CONSUME
           ▼                               ▼
┌──────────────────┐          ┌───────────────────────────────┐
│  PHP Magento     │          │  Go Service (magento-go)           │
│                  │          │                                │
│  MySQL Magento   │          │  Go DB (owned, flat schema)   │
│  (fully owned)   │          │  CQRS read projections        │
│                  │          │  customer_flat, order_summary  │
│  ← Magento must  │          │                                │
│    publish ALL   │          └───────────────────────────────┘
│    state changes │                         │ PUBLISH
│    as events →   │◀────────────────────────┘
└──────────────────┘  (compensating events)

In Option C, both systems are completely isolated. Communication happens exclusively through the event bus. Each service owns its database entirely.

What Option C Requires That Option B Does Not

The fundamental difference is who publishes events:

Option B (CDC)Option C (Event Bus)
Who publishes changes?Debezium reads binlog automaticallyPHP Magento must write to outbox table
PHP Magento code changes?❌ None requiredMandatory — every state change needs an event
Risk if PHP team is slowZero (CDC is infrastructure)High (Go DB will be missing data)
Latency of sync50–200ms (binlog lag)10–100ms (if Kafka is healthy)
Event replayYes (binlog history)Yes (Kafka retention)

Option C is the right long-term target architecture. But it requires the PHP Magento team to reliably emit domain events for every state change — customer updates, order status changes, inventory adjustments, promotion applications. Missing even one event category means Go’s database silently diverges from Magento’s.


Full 3-Way Comparison Matrix

DimensionOption A: Shared DBOption B: CDC + OutboxOption C: Full Event Bus
Consistency modelStrong ACID ✅ACID writes, eventual reads ⚠️Eventual consistency ❌
Schema couplingTight — Magento owns ❌Reduces per domain ⚠️Loose — event schema ✅
Write conflict riskHigh — race conditions ❌Low — owned writes ✅None — isolated ✅
EAV performanceUnchanged ❌Flatten per domain ⚠️Full flatten ✅
Independent scalingNo ❌Partial ⚠️Full ✅
Rollback Go serviceTrivial — flip routemode ✅Easy per domain ✅Hard — event schema backward compat ❌
Time to productionRunning now ✅3–6 months ⚠️9–18 months ❌
Magento code changesNone ✅None (CDC reads binlog) ✅Required — event publisher ❌
New infrastructureNone ✅Debezium + Kafka ⚠️Kafka + Schema Registry + CQRS ❌
Debug complexityLow ✅Medium ⚠️High — trace across hops ❌
Team skill requiredLow ✅Medium ⚠️High — distributed systems ❌
Fault isolationNone — 1 DB = all fails ❌Partial ⚠️Full ✅
Checkout transaction safetyACID ✅Saga required ⚠️Saga + compensating tx ❌
Inventory oversell riskNone (ACID) ✅Low if CDC lag < 100ms ⚠️High if consumer lags ❌
Data replay / auditNone ❌Partial (Outbox) ⚠️Full (Kafka retention) ✅

Risk Table Per Option

Option A — Shared DB Risks

RiskLikelihoodImpactMitigation
Magento upgrade breaks Go structHigh (every upgrade)Service crashSchema pinning + CI diff check
Write race condition on auth tablesMediumData corruptionTable ownership policy, Go read-only
DB bottleneck at scaleHighBoth systems slowRead replica for Go
Permanent distributed monolithCertain without deadlineArchitectural debtHard Phase 2 deadline

Option B — CDC Risks

RiskLikelihoodImpactMitigation
CDC pipeline failureLow (Debezium HA)Go DB out of syncLag monitoring + alerting
Duplicate event deliveryMediumDuplicate recordsIdempotency key on all consumers
MySQL binlog disk pressureLowStorage costbinlog retention policy
Temporary dual-write windowMediumData driftOutbox pattern from day one

Option C — Full Event Bus Risks

RiskLikelihoodImpactMitigation
Magento emits incomplete eventsVery HighGo DB missing dataIntegration test suite, mandatory
Event schema breakHigh (per business change)Consumer crashSchema Registry + backward compat
Saga failure → inconsistent orderMediumRevenue impactDead-letter queue + manual review
Event storm during flash saleMediumKafka backpressureTopic partitioning + consumer lag SLA
Read-your-own-write lagHighPoor UX after user actionOptimistic UI updates

⚠️ Critical for Option C: PHP Magento must write to an outbox table within the same database transaction as the business state change. This is often implemented incorrectly — publishing directly to Kafka after the DB commit creates a dual-write gap. If the Kafka publish fails, the event is lost silently and Go’s database diverges permanently.


Decision Framework — Which Option Is Right for You?

Q1: Does your team have 2+ engineers with distributed systems experience?
  └─ NO  → Stay on Option A, add guardrails (schema pinning, read-only policy)
  └─ YES → Q2

Q2: Is your inventory oversell tolerance exactly zero?
  └─ YES → Option B (CDC maintains ACID writes, no eventual consistency risk at write time)
  └─ NO  → Option B or C are both viable

Q3: Do you need Go to scale completely independently of Magento's DB?
  └─ NO  → Option B is sufficient and recommended
  └─ YES → Q4

Q4: Can the PHP Magento team commit to building and maintaining event publishers?
  └─ NO  → Option B is required (CDC needs no PHP changes)
  └─ YES → Option C is viable (timeline: 12–18 months)

How long does a Magento database migration to Go take?

Option A (Shared DB) is running now — zero additional time. Option B (CDC + Debezium separation, domain by domain) takes 3–12 months depending on team size and domain complexity: Auth/OIDC in 1–2 months, Customer read in 2–3 months, Cart/Checkout after Saga patterns are proven. Option C (Full Event Bus) requires 12–18 months plus PHP Magento event publisher development before Go’s database can be fully independent.

What is the Outbox Pattern in microservices?

The Transactional Outbox Pattern solves the dual-write problem in event-driven systems. Instead of writing to the database AND publishing to Kafka in two separate operations (which can fail independently), you write the event to an outbox table inside the same database transaction as the business state change. A background processor (OutboxProcessor) then reads the outbox table and publishes events to Kafka at regular intervals — typically every 500ms. This guarantees that if the DB commit succeeds, the event will eventually be published, eliminating silent data loss.

Can I use Debezium with Magento MySQL without changing PHP code?

Yes. Debezium is a Change Data Capture (CDC) tool that reads MySQL’s binary log (binlog) directly — it does not require any application code changes. For Magento, you configure the Debezium MySQL connector to monitor specific tables (customer_entity, catalog_product_entity, etc.) and stream every INSERT/UPDATE/DELETE to Kafka or Redis Streams. The prerequisite is that MySQL binlog is enabled on your Magento database server (log_bin = ON), which is typically already the case on production MySQL instances configured for replication.

Domain Priority — What to Separate First

Not all domains carry equal risk. Migrate in this order regardless of whether you choose Option B or C:

DomainPriorityWhyRisk
Auth / Token (auth/)FirstGo already owns token logic, zero Magento dependencyLow
OIDC (oidc/)FirstFully Go-owned, no Magento data neededLow
Wishlist (wishlist/)SecondSimple domain, small data footprintLow
Customer read (customer/)SecondFlatten EAV → 5× read speed, write still proxiedMedium
Customer writeThirdRequires Saga with Magento for address/profile syncMedium
Cart / Checkout (cartcheckout/)LastHighest business risk, requires mature Saga patternHigh

Never migrate Cart/Checkout to a separate DB until Saga patterns are proven in production on lower-risk domains.


Infrastructure Checklist Before DB Separation

Before committing to Option B or C, validate these prerequisites:

required_before_option_b:
  - mysql_binlog_enabled: true          # Check with: SHOW VARIABLES LIKE 'log_bin'
  - debezium_or_dms: deployed           # Debezium embedded or standalone
  - kafka_or_redis_streams: deployed    # Message transport for CDC events
  - outbox_table: created_in_go_db      # magento_outbox table schema defined
  - idempotent_consumers: implemented   # Every consumer has idempotency key check
  - cdc_lag_monitoring: configured      # Alert if lag > 500ms

additional_for_option_c:
  - php_event_publisher: implemented    # Magento module writing to outbox table
  - schema_registry: deployed           # Avro or Protobuf schema management
  - saga_orchestrator: stable           # Proven in production on non-critical domain
  - dead_letter_queue: configured       # For failed saga compensation

0–6 months (NOW)             6–12 months                12–24 months
────────────────             ───────────                ────────────
Option A + Guardrails   →    Option B: CDC              Option B → C (gradual)
                              Auth/OIDC DB first
Table ownership policy        Debezium streaming         Only when PHP team
Schema pinning CI check       Flatten EAV reads          can publish events
routemode shadow mode         Wishlist + Customer        Saga pattern proven
Hard Phase 2 deadline set     read separation            Cart/Checkout LAST

See Also

Series Deep-Dives

External References