We knew splitting the database would be hard—data consistency across services, foreign key constraints disappearing, all that theory. What surprised me was how much of our business logic lived in database triggers and stored procedures we'd forgotten existed.
Our first attempt: split users, orders, inventory, and payments into separate schemas on the same Postgres instance. Felt safe. Then we discovered 23 stored procedures doing cross-schema joins. The product catalog service needed user tier information for pricing. The order service checked inventory in real-time. Everything was coupled at the database layer.
We spent January 2025 untangling that mess. Each service needed its own copy of data it depended on. The orders database got a denormalized user_tier column. We set up event streaming with Kafka so when a user's tier changed, that event updated the orders database. Eventual consistency became our new reality.
The scariest part? The first migration to truly separate databases. We did it for the payment service on a Tuesday morning. Had a rollback plan. Tested it seventeen times in staging. Still, watching real customer payments flow through the new setup while having zero ability to do database joins if something went wrong—my hands were shaking.
It worked. But the lesson stuck: in microservices, data boundaries are harder than code boundaries. Plan for that upfront or pay for it later.