Sebastian Alvarez
Back to Blog

Let’s Talk Microservices: Lessons Learned in the Real World

September 1, 2025
By Sebastian Alvarez
Microservices
Architecture
Cloud
API Gateway
DevOps
Docker
Let’s Talk Microservices: Lessons Learned in the Real World

Introduction

In my career as a software engineer, I’ve had the chance to work with very different architectures. I’ve refactored legacy CMSs into custom backends that later evolved into microservices. I’ve joined huge companies where microservices are the norm from day one. And I’ve also been part of projects where the choice to go microservices too early was a mistake, and we had to migrate back to a monolith.

These experiences taught me an important lesson: microservices can be the right or wrong decision depending on context. They’re not a silver bullet. Used well, they give flexibility, scalability, and speed. Used badly, they add unnecessary complexity.

One point I’ve learned through practice: a system can start as microservices only if it already has a well-defined logical space inside an established industry. For example, a banking core system, where scalability needs and separation of concerns are clear from the beginning. Otherwise, most projects benefit from starting with a monolith and evolving later.

In this blog, I want to share an educational perspective: good practices for designing microservices, when they make sense, and how to build them in a way that truly adds value. To make it more visual, I’ll use a banking system as our example throughout.


The Banking System Analogy

Think of a digital banking platform. Some modules clearly demand more scalability than others. For instance:

  • Transactions Module → Needs to handle millions of transfers and payments in real time. Requires scalability, monitoring, and fault tolerance.
  • User Profile Module → Manages user data, preferences, and settings. Important, but less traffic-intensive compared to transactions.
  • Notifications Module → Sends alerts, push notifications, and emails. Workload can spike, but is often asynchronous.

This makes banking a great analogy: not every service has the same scaling needs. Microservices allow you to scale the transaction engine independently without overprovisioning less demanding modules like user profiles.


Scalability: Vertical vs. Horizontal

When we talk about scalability in microservices, there are two main strategies:

  • Vertical scaling: Giving a single machine more power (CPU, RAM, faster storage). It’s simple, but limited — there’s only so much hardware you can add.
  • Horizontal scaling: Running multiple instances of a service across different machines or containers. This approach is almost infinite in theory, and microservices make it natural: just spin up more containers of the transaction service during peak hours.

In our banking system, vertical scaling might be enough for the customer profile service, while the transactions module requires horizontal scaling to ensure uninterrupted performance at high demand.


Independent Deployments

One of the strongest arguments for microservices is the ability to deploy each service independently.

In the banking example:

  • You may need to push multiple daily updates to the Transactions Module for bug fixes, compliance updates, or new payment features.
  • Meanwhile, the User Profile Module may only change once a month.

Independent deployments avoid bottlenecks and let teams move at different speeds. However, this only works well if CI/CD pipelines, proper testing, and versioning are in place. Otherwise, deployments become chaotic instead of empowering.


Cloud, Docker, and Portability

Microservices thrive on portability, and this is where Docker and Kubernetes shine.

  • Docker ensures that every service runs in the same environment, from developer laptops to production. No more “it works on my machine”. Each microservice is packaged with its dependencies, which makes onboarding and debugging easier.
  • Kubernetes adds orchestration: it manages scaling (more containers for the Transactions Module during peak hours), rolling updates, self-healing, and service discovery. In banking, this means the system can handle Black Friday-like transaction spikes automatically without downtime.

Together, these tools are the backbone of modern microservices: they make services portable, resilient, and cloud-native.


Databases: To Share or Not to Share

A common mistake in microservices is trying to share the same database. This creates tight coupling and coordination nightmares.

Instead, each service should own its data. For example:

  • The transactions service owns its transaction history database.
  • The customer service owns customer records.

A good practice is to give each service its own database to reduce coupling. For example, the Transactions Module might use a high-performance SQL database optimized for financial records, while Notifications could use a NoSQL DB for flexibility.

Services communicate through APIs or events, not by peeking into each other’s tables. This ensures data consistency, clearer responsibilities, and easier scalability.


Security and Access Control

In microservices, security must be centralized in design, even if distributed in implementation.

Instead of each service handling its own authentication logic, most teams use:

  • OAuth 2.0 / OpenID Connect for centralized authentication.
  • API gateways to enforce policies (rate limits, IP whitelists, etc.).
  • Service-to-service authorization (e.g., with OPA or service mesh) for internal trust boundaries.

In banking, this is critical: a Notifications service should never be able to directly access user balances. Proper token scopes and policies enforce this separation of concerns.


Standards and Consistency

Microservices only work well if they follow common standards. Otherwise, integration becomes chaotic.

Some best practices include:

  • Consistent API response formats across services. For example:
{
  "data": object | array,
  "count": number, // Just for array-based responses
  "message": string
}
  • Naming conventions for endpoints:
    • Use clear, versioned paths: /api/v1/transactions, /api/v1/customers.
    • Use plural nouns for resources, and consistent verbs for actions if necessary (e.g., /api/v1/transactions/transfer).
  • Standardized error handling:
    • Each error response should include consistent fields, for example:
{
  "error_code": "INSUFFICIENT_FUNDS",
  "message": "The account balance is too low for this transaction",
  "details": {
    "account_id": "123456789",
    "attempted_amount": 500
  }
}
  • Consistent logging and error-handling formats.
  • Versioning strategies for APIs to avoid breaking clients.

These standards ensure that different teams can build services independently while still making them interoperable and predictable, reducing surprises during integration and testing.


Testing in Microservices

Testing is often underestimated in microservices, but it’s where projects succeed or fail. Unlike monoliths, where you can run a single suite of tests, microservices require multi-layered testing strategies:

  1. Unit Tests → Validate logic inside each service (e.g., transaction fee calculation).
  2. Integration Tests → Validate how services interact. Often requires mocking (e.g., simulate the User Profile service when testing Transactions).
  3. End-to-End (E2E) Tests → Validate real-world workflows across multiple services. For a bank: “A user makes a transfer, balance updates, and a notification is sent.”

To achieve this, teams often spin up controlled environments using Docker Compose or Kubernetes namespaces, where services interact in isolation. Contract testing tools (like Pact) help ensure that APIs between services remain compatible.

In my experience, the teams that invested in strong integration and E2E testing saved months of debugging later. Without it, you end up with microservices that work in isolation but fail when combined.


Organizational Alignment

Microservices only make sense if your team structure supports them. Conway’s Law tells us that software architecture reflects organizational communication.

  • If you have a dedicated Transactions team, a User Profile team, and a Notifications team, microservices can map perfectly to this structure.
  • If you only have a small team of three developers, splitting into 10 microservices will create unnecessary overhead.

In my career, I’ve seen both extremes:

  • Large organizations where microservices enable parallel work across hundreds of developers.
  • Startups where the microservice dream slowed everything down because there simply weren’t enough hands to manage it.

Conclusion

Microservices are not a magic solution — they are a tool. They work best when:

  • The system has parts with very different scalability requirements.
  • Independent deployments accelerate business needs.
  • Teams can enforce consistency, security, and testing discipline.

From my own journey — migrating CMSs, scaling enterprise systems, and sometimes rolling back to monoliths — I’ve learned that the key is context. Microservices shine in large, well-defined domains, but they are overkill for small or rapidly changing products.

If you design them with scalability, standards, and testing in mind, microservices can transform complex industries like banking into resilient, scalable, and maintainable systems.