Follow this blog

Software engineering, design, and psychology

Decoupling Responses from Requests: Event-Driven Approach | Microservice Architecture — Ep. 26

Traditional client-server communication is based on request-response paradigm:

  • send a request
  • show loading state and wait
  • get the response
  • update client state

However, microservice architecture does not go along well with it because of two things:

  • multiple network hops (high cumulative latency)
  • request-response propagation through service chains
    So many requests turn into small distributed workflows.

Consider a simple subscription-based video platform. One frontend and three services: Subscriptions, Payments, Recommendations.

The typical “subscribe” workflow may look like this:

  1. Client calls Subscriptions.
  2. Subscriptions calls Payments.
  3. Payment succeeds.
  4. Recommendations builds a personalized movie list.
  5. Client fetches recommendations.

Where does this orchestration live?

  • In the frontend? → tight coupling to backend internals
  • In a BFF? → centralized coordination and sequential bottlenecks
    Both approaches are fragile due to excessive coupling. And if each service talks to each in request-response paradigm, it becomes a tough task under load, having to support hundreds and thousands of open connections.

An alternative is to decouple responses from requests using events. Instead of calling each other and waiting for long responses, services communicate through a message broker:

  • Subscriptions emits ‘UserSubscribed’
  • Payments consumes it and processes payment
  • Payments emits ‘PaymentCompleted’
  • Subscriptions and Recommendations react independently
  • Recommendations emits ‘RecommendationsReady’
  • Some Notifications service may send an email or a live update to the frontend client

With event-driven architecture we achieve three things:

  • services no longer keep open connections, waiting only for confirmation from message broker
  • if some service breaks, requests are not lost, but can be picked up from the message broker by another service instance
  • there is no node concerned with process coordination — coordination complexity is moved to communication space
  • the system becomes asynchronous: frontend clients receive immediate feedback and unlock other activities while complex workflows are processed in background

The tradeoff — we lose immediate consistency. The benefit — we gain scalability, resilience, and simple service boundaries. This shift, from waiting for results to reacting to events, is one of the key mindset changes in distributed systems.

Auto-Scaling Groups and Load Balancers | Microservice Architecture — Ep. 25

The purpose of microservice architecture is durability and scalability. Durability means a service continues to serve requests during failures or peak traffic. Scalability means achieving durability with the least resources possible, making the service cost-efficient.

Most modern microservices typically run on small virtual machines with limited vCPU and RAM. These instances are cheap, but still powerful enough to handle thousands of requests per minute.

However, running a single instance of a service is never enough, as hardware failures are rare but inevitable — even in the best data centers. To maintain durability, at least two instances of a service must run in different data centers (“availability zones” in AWS terminology).

Another issue is traffic spikes. When load increases, a single instance becomes incapable of handling all requests, so multiple instances become a necessity.

In practice, service load changes dynamically, e. g. low at night and ten times higher of that during business hours. For cost efficiency, services are usually placed in auto-scaling groups. A dedicated agent monitors metrics such as CPU usage or request rate and automatically adds or removes instances as load surpasses predefined thresholds. Scaling achieved through changes in a number of small instances is called horizontal scaling.

Horizontal scaling introduces another challenge: how do clients know which instance to talk to?

An API gateway routes requests to services, but it should not track which instances currently exist or which ones are healthy. That responsibility is delegated to load balancers — lightweight proxy services that:

  • track active service instances and their IP addresses
  • monitor instance health and availability
  • distribute load evenly across instances

As load balancers expose a stable endpoint (often a static IP or DNS name), API gateways can simply route requests to them — offloading instance discovery and traffic distribution to the combined work of load balancers and auto-scaling groups.

Why API Gateways Became a Thing (Part 2) | Microservice Architecture — Ep. 24

The key feature of API Gateways is the access to all system requests, services they target, and outgoing responses. This position enables capabilities that go beyond simple routing.

Let’s peek into these capabilities.

API Versioning
If multiple API versions exist, the Gateway can forward requests targeting specific API versions to the matching service implementations, centralizing backward compatibility.

Canary Deployments
The Gateway can route portions of traffic to new service instances running updated code. As it has access to request and auth data, it can do so based on routes, headers, regions, or tenant attributes.

Basic validation
The Gateway may check for oversized payloads and validate basic schema constraints before traffic reaches services, reducing unnecessary system load.

Request / Response transformation
The Gateway may enrich requests, e. g. inject tenant data or correlation IDs. In the same manner it can reshape responses, e. g. by hiding internal fields from response payloads.

Data aggregation
In simple cases, the Gateway can split a request across multiple services and combine responses before returning them, reducing the need for separate aggregation services.

Caching
The Gateway can cache common GET requests, improving latency for high-traffic endpoints and protecting downstream services from excessive load.

The API Gateway is not just a router and observer — it is a policy enforcement and traffic management layer.

Why API Gateways Became a Thing (Part 1) | Microservice Architecture — Ep. 23

As we discussed earlier, microservices tend to accumulate a lot of repeated concerns unrelated to their responsibilities. If something repeats across services, we should try to move it into a separate service that clients hit first before talking to downstream services — some Gateway.

What can we hand off to the Gateway?

Routing
Public-facing services have IPs which are prone to change. The Gateway provides stable routes, discovers service IPs and forwards requests to the right targets.

Protocol translation
Services do not need to support any protocol. Teams pick the most appropriate one, and the Gateway translates requests and responses as needed.

Authentication and tiered access
Domain services should not parse tokens or know about subscription tiers. The Gateway validates credentials, checks access levels, and can short-circuit unauthorized requests before they reach downstream services.

TLS termination
If traffic enters through a single point, TLS can be terminated at the Gateway. Internal services may communicate over simpler internal channels (though in high-security environments, end-to-end encryption may still be required).

Request logging and monitoring
As the Gateway handles all requests, it can centrally log user activity and provide metrics.

Request throttling
The Gateway can track whether a client puts excessive load on the system and stop propagation of frequent requests. And as it sees authentication context and system-wide traffic patterns, it can enforce global rate-limiting strategies — individual services cannot do that effectively.

In short, the Gateway provides a stable application programming interface to an unstable internal world — it is the API Gateway. It hides topology, enforces global policies, and lets domain services focus on business logic. That is why it became a foundational pattern in microservice architecture.

Cross-Cutting Concerns in Microservices | Microservice Architecture — Ep. 22

Let’s return to our migration from a monolith. Imagine we’ve successfully split it into dozens of microservices. Some services are internal, some face clients.

The company grows, the number of clients increases — along with their demands:

  • new devices
  • new protocols
  • new response formats
  • new pricing tiers
    Multiple services, combined with lots of clients lead to complex inter-service relations.

If we take a naive approach and extract domain slices into services along with everything required to provide proper access — every team will be solving the same set of problems.

Each service now has to:

  • support multiple communication protocols
  • handle authentication
  • evaluate tenant tiers
  • manage throttling and load balancing
  • collect logs, metrics, and tracing data

There is one more problem — data aggregation. Many endpoints need data from multiple services. To reduce latency, we introduce caching, but:

  • some queries are hot, others are rarely used
  • some queries require near real-time freshness
  • different clients may need different response shapes
  • different clients may want to combine data from different domains

Aggregation services risk becoming bloated and complex, with clusters of similar endpoints coupled to specific client needs, and excessive duplication of both code and cached data.

How can we address these problems?

Micro-Frontends and the Illusion of Isolation | Microservice Architecture — Ep. 21

While the key idea behind micro-frontends is the same as for microservices — independent development and deployment — micro-frontend architecture presents very different problems.

No real isolation.
Backend microservices run in separate processes — often on separate machines. Micro-frontends share the same browser tab: the same DOM, CSS namespace, event system, storage, and main thread.

Limited deployment independence.
Backends integrate over the network — but micro-frontends integrate in memory. They depend on a shell application, shared routing and design systems: braking changes in those layers affect everyone.

Shared data is unavoidable.
A lot of frontend state is cross-cutting: feature flags, session, auth, cart contents. We can modularize code, but cannot pretend data ownership is cleanly separated.

Shared and constrained hardware.
On the backend, a slow service does not block CPU for others and can be easily scaled. In the browser, components compete for the same main thread, often on low-end mobile devices. Performance budgets are a cross-team concern.

Integration problems are very prominent.
Backend failures degrade responses. With proper loading and fallback states in UI, users may not even notice. Frontend failures degrade user experience: broken layout, flicker, inconsistent interactions — all seen immediately.

Splitting responsibility without breaking UX is the real challenge of micro-frontends.

What are Micro-Frontends? | Microservice Architecture — Ep. 20

Historically, user-facing applications were rendered on the server — the same place where all logic resides. The backend processed requests and generated HTML dynamically.

Around 2010, single page applications (SPA) emerged. HTML started being generating in the browser using data from backend, which enabled complex state management and rich UI interactions.

Today, we see a partial return to server-side rendering (SSR) for performance and SEO reasons. Still, state orchestration and smooth UX remain complex enough to require dedicated frontend expertise.

Just like backend systems, frontend codebases often start as monoliths. And just like in the backend world, once a frontend team grows beyond ~5 engineers, coordination becomes harder, release cycles slow down, and ownership becomes blurry, calling for split teams — the micro-frontend approach.

Usually this is achieved by having a shell application that provides layout, routing, and shared infrastructure. Independent frontend teams work on separate pages or feature slices, that are composed at runtime.

Core principles of micro-frontends:

  • Isolation
    Unlike backend microservices, code from different teams ends up in the same browser page — so teams have to namespace CSS classes, browser events, cookies and local storage items.
  • Resilience
    A robust frontend prioritizes server-side rendering and progressive enhancement. Teams should rely on server-side rendering, using client-side JS to enhance UX.
  • Technology agnosticism
    Teams should be able to choose their frontend stacks, which should not affect performance or user experience of the whole SPA.

Microservice thinking is not limited to backend systems. The idea of independent teams working on well-defined, isolated components applies to any system divisible by such components.

Team Autonomy That Actually Scales | Microservice Architecture — Ep. 19

As we learned in the previous episode, not every decision benefits from decentralization.

When microservices use technologies different from the original monolith, the organization pays:

  • a one-time cost to set up each service
  • a recurring cost to operate and support it

The cost of building actual features, however, stays roughly the same regardless of language or paradigm.

So autonomy should be tiered — unify decisions that reduce operational and coordination overhead!

1. Strict tier
Single standards to avoid repeated work across teams:

  • infrastructure — cloud provider, core services, machine types
  • API standards — protocol, versioning and deprecation rules (might differ by service class)
  • monitoring and alerting — shared logging, metrics, and alerting conventions
  • security policies — access control, secret rotation, PII handling

2. Relaxed tier
A small set of options (2–5 choices) to balance flexibility and cognitive load:

  • programming languages and frameworks
  • messaging technologies
  • database technologies

3. Guardrail tier
Default “golden paths” teams follow unless they consciously opt out:

  • recommended tech stacks
  • starters and templates
  • base CI/CD pipelines

4. Free tier
Fully team-owned decisions where autonomy improves effectiveness without added recurring costs:

  • internal quality practices
  • release process
  • release schedule

Tiered autonomy reduces infrastructure and support costs, while giving teams enough freedom to organize their work effectively.

Why You Don’t Want Complete Team Autonomy | Microservice Architecture — Ep. 18

Microservice team autonomy means freedom to choose technologies: languages, frameworks, databases, infrastructure.

Imagine splitting a large monolith into 10-20 microservices. Each team picks a stack different from the original system — and different from each other.

This leads to:

  • new folder structures
  • new code style conventions
  • new linting rules
  • new test harnesses
  • new database schemas
  • new CI/CD pipelines
  • new infrastructure definitions
  • new monitoring and alerting setups

And this can turn very problematic:

  • Most of these decisions require thorough analysis with long discussions — even within a single team.
  • Engineers face a steep learning curve as the whole tech stack differs from the one they are used to.
  • API boundaries become harder to reason about when teams mix REST, GraphQL, RPC, and custom protocols.
  • Infrastructure complexity explodes — the teams might start spending more time on configuration than on writing new features!

Microservices aim to enable independent teams and faster delivery — but not every decision benefits from decentralization.

What do you think might be a possible solution here? Share your thoughts in the comments!

I’m writing a full series on microservice architecture — from base definitions to hands-on topics like testing, deployment, and observability.
Follow or connect if you want to continue the series, or read them on my blog: https://mishurovsky.com/blog/tags/microservices-event-driven-architecture/

DRY vs Microservices: When Reuse Becomes a Liability | Microservice Architecture — Ep. 17

“Don’t Repeat Yourself” principle sits at the core of software engineering. We usually structure our code to have as few to changes as possible when requirements shift.

This approach is right inside any single microservice — use it! But it gets trickier when DRY is applied across microservices. Not every duplicated piece of logic should be extracted into a shared library.

Remember, the goal of microservice architecture is independent deployment. Shared libraries work against that goal and introduce coupling:

  • Updating a library forces coordinated releases across multiple services (who owns the library and coordination then, by the way?).
  • A bug in the library makes all dependent services vulnerable.
  • Frequent library updates open a potential for version conflicts and dependency hell.

So what if logic really needs to be shared?

If it is cross-service communication logic, sharing code can be a good tradeoff. Libraries or code generation to keep contracts in sync (schemas, clients, message definitions) usually increase development speed rather than hinder it.

If it is business logic, this might indicate a boundary problem. Combine microservices into a single, more cohesive one — or extract that logic into a dedicated service.

If introducing a new service is undesirable, the sidecar pattern is a possible solution. Move shared logic into a separate process running co-host with each service instance. This way service and sidecar lifecycles get separated, avoiding tight compile-time dependencies.

In microservices, duplication is often cheaper than coordination.

Problems of the “One Database per Microservice” Approach | Microservice Architecture — Ep. 16

As discussed in the previous post, the proper approach of data ownership in microservice architecture is for each service to have its own database hidden behind a network API.

While this approach solves independent service development and deployment, it introduces strong downsides:

  • Accessing foreign data now requires a network call, typically adding 100 – 200 ms of latency.
  • Cross-service joins become inefficient: data must be fetched from multiple services, normalized, and only then combined.
  • Cross-service transactions get much more complicated — some services may use storage technologies that don’t support transactions at all!

How to cope with these problems?

  • Cache foreign data locally to reduce network calls. This improve latency, but introduces tough questions: what to cache and when to invalidate.
  • For complex joins, create a dedicated microservice that gathers data on schedule and stores join results in its own database (a benefit here is that it can keep and provide results of previous computations).
  • For cross-service transactions, use sagas. Each service defines forward and compensating actions, coordinated either via orchestration (by a separate coordinator service) or choreography (event-driven coordination configured for each participating microservice).

These solutions are not simple. The “one database per service” principle trades local simplicity for system-level complexity — but it remains the most scalable and sustainable approach for microservice architectures.

One Microservice — One Database | Microservice Architecture — Ep. 15

The core idea of microservice architecture is decoupling: separate teams, codebases, lifecycles. Yet often it is still tempting to share a database between multiple services, especially when the system is relatively small. But it is a wrong decision.

Why shared databases break microservices:

  1. Microservice A cannot write to tables owned by microservice B, as each microservice must own its data undivided.
  1. Microservice A cannot read tables owned by microservice B, as any schema change, table / row permission update, or migration can silently break downstream services.
  1. Microservice A cannot keep its data in a separate table of microservice B’s database. If the B changes DB technology or scaling strategy, the tables of A have to be moved elsewhere, but by who owns the migration? It is especially complex issue once there are more than two services involved.
  1. With direct DB connections, the load on IO from some fast growing service might become so intensive it would degrade operation of unrelated services.

This is why each microservice needs its own database. All access to that data must go through the service’s API or events, and never through direct database connections.

Book review: “Data Pipelines Pocket Reference”, James Densmore

Software engineering is all about manipulating data. A big portion of software engineer’s attention is drawn to collection data from users and presenting it back to them in a useful form. However, there is another side of data — the kind that is not produced by software users but only consumed by them. Here, we aim for the goals of achieving a single source of truth, data validity and availability, and enabling performant processing (for analysis or presentation).

To get a better grasp of the tooling for working with such data, this week I read Data Pipelines Pocket Reference by James Densmore. The book focuses on the modern ELT (Extract-Load-Transform) approach, as well as EtLT (with ‘t’ for generic non-business-related data transformation).

It turned out to be a very practical pocket guide indeed. Each section of the book dedicated to data extraction, loading, and transformation is supplied with clear code snippets in Python. The snippets demonstrate means to connect to essential services, such as databases, AWS S3, Amazon Redshift, Snowflake, Apache Airflow, as well as basics of data manipulation.

I liked two things. First, these snippets feel production-ready. Surely, they feature no robust logic, but they are sufficient to start moving data around, running validations and applying transformations. Second, the author not only focuses on interaction with services, but
also provides some tricks of data processing and validation. In particular, there is a neat data testing framework based on separate Python scripts for each check, which can be integrated into Airflow workflows. The approach, while being quite lean, requires a certain mindset to arrive at, so this bit of knowledge was one of the things that saves time and builds a scalable data processing foundation.

That said, I think this book lacks example that are closer to real practice. It would benefit from a companion GitHub repository with a substantial dataset to run ELT against, in addition to the primitive data samples from the book which take no more than 10 rows and 5 columns in a single SQL table. The book also misses any in-depth discussions, making it a pocket reference, indeed.

What it covers:

  • Data roles: data engineering, data analytics, and data science
  • Types of pipelines: ETL vs. ELT vs. EtLT
  • Overview of tools for each ELT step and their orchestration
  • Minimal instructions for setting up data ingestion and transformation
  • Approaches to pipeline orchestration
  • A framework for data validation
  • Building pipelines with monitoring and maintenance in mind

Verdict: 4 / 5 — a good reference to start building simple ELT pipelines in a day, which is likely exactly what a general software engineer would want if data engineering is not their primary area of specialization

How to Migrate from a Monolith to Microservices (Without Regretting It) | Microservice Architecture — Ep. 14

When an architectural change this big is agreed upon, it is natural to perform migration of the whole monolith at once. Freeze all current development, develop a plan, split tasks and start. But beware!

If the decision to migrate is made, it is likely we already have a large codebase and an excessively large team that has lost coordination and velocity. The business will not accept months without new features, either.

The right way is to go incremental. Create a small team of experienced engineers, isolate a narrow component for migration, and set no hard deadlines. You will uncover unexpected issues early — but with limited scope, the cost of mistakes stays low and progress is still clearly visible.

How to execute the migration?

  1. Define API of the component to be extracted.
  2. Ensure its thorough automated test coverage with a 100% pass rate.
  3. Isolate the component by removing dependencies on the monolith.
  4. Extract into a microservice and ensure all tests passing.
  5. Put the microservice behind a facade (e. g., an API gateway).
  6. Route part of production traffic to it.
  7. Monitor the service under the real load and fix issues.
  8. Remove the extracted component from the monolith.
    This approach is commonly known as the Strangler Fig Pattern.

Microservices migrations fail most often not because of technology — but because the change must be both technical and organizational.

Where to Split? | Microservice Architecture — Ep. 13

A classic question when designing microservices: what defines a good boundary?

One approach is to split business capabilities. For an online store they can be:

  • Product Catalog
  • Search
  • Reviews
  • Cart
  • Billing
  • Shipping

Capability-based splitting usually leads to very stable services, as business needs evolve slowly and rarely require large-scale changes.

Another approach is to split by technical domains:

  • Core — unique functionality of this particular business (e. g. Product Catalog)
  • Supporting — industry-common core helpers (e. g. Cart, Billing, Shipping)
  • Generic — non-unique non-specific functionality (e. g. Auth, Search, Payment, Image Processing)

This model is more intuitive for engineers. It also makes it easier to decompose large domains into smaller services when scale or ownership demands it.

Both approaches focus on cohesion and loose coupling and achieve quite stable microservice boundaries under changing requirements.

Book review: “Web Scraping with Python”, Ryan Mitchell

Recently I faced a challenge of designing a web crawling and scraping system. To build context, I started my work by reading Web Scraping with Python: Data Extraction for the Modern Web by Ryan Mitchell (3rd edition, revised in 2024).

The book turned out to be a very pleasant read. The author’s approach is well structured with chapters going from simple practical tasks and legal overview to advanced considerations such as Natural Language Processing and race conditions in distributed scraping systems. Code snippets are concise and useful — I can imagine them being used in a small scale production system. In two days, I managed to build a solid understanding of common approaches, architectures, problems, and solutions in this field.

That said, the content is not without its flaws. The section on JavaScript and SSR section is so outdated it is almost hilarious. Mentions of Dynamic HTML, jQuery and AJAX calls are appropriate for a book written around 2010, but not for a revised version from 2024. Nonetheless, even this section is useful at a conceptual level: modern SPAs achieve the same goals as early 2000s web applications that generated dynamic HTML server-side and sent it to browsers.

The ease with which I read this book was strongly influenced by my existing knowledge of the web. Over the years, I have built a solid foundation in HTML and CSS, JavaScript and Python, APIs, application architecture, and networking — all of which helped me clearly see the connections between the author’s ideas. However, the book should still be accessible to any technical reader, thanks to its clear explanations and practical code examples.

What it covers:

  • Principles of web technologies
  • Legal and ethical considerations
  • Common scraping use cases
  • Building web crawlers and scrapers
  • Crawling strategies
  • Transformation and validation of collected data
  • Parsing text and image documents
  • Scraping traps
  • Distributed scraping systems

Verdict: 4.5 / 5 — a go-to practical guide for those planning to build their own scraping system.

Principles Behind Good Microservice Boundaries | Microservice Architecture — Ep. 12

  1. Single Responsibility. Consumers of a service need to clearly understand its purpose.
  2. High Cohesion and Exhaustiveness. All related functionality has to end up in the same service.
  3. Low Coupling. Services should minimize dependencies on others — even if that means duplicating supporting code. Big codebases for microservices are OK as long as the services are cohesive and centered around a single business capability.

Imaging we have a patient data management system for hospital intensive care units (ICUs), built as a monolith. It manages patient information, prescriptions, treatment history, vital parameters, and provides a dashboard with current state and therapy.

❌ Wrong decomposition:

  • dashboard
  • care plan
  • monitoring

Why? Consider we add a new drug into the system. All services must change:

  • care plan — to prescribe it
  • monitoring — to display past treatments with the new drug
  • dashboard — to show current therapy

✅ Better approach:

  • drugs
  • vital parameters
  • timeline (treatment plans, current state, history)

Why? Available drugs and vital parameters change in one place, while evolution of timeline does not affect other services. Each service owns a stable business concept, not a UI page.

Good microservice boundaries reduce change coordination, not just code size.

Benefits and Challenges of Microservice Architecture | Microservice Architecture — Ep. 11

The core of microservice approach contains two ideas:

  • narrow business domains
  • small and independent teams (2-pizza team size)

So, splitting monoliths into microservices immediately brings benefits:

  • smaller codebases — easier to comprehend, faster to change
  • higher cohesion — easier to comprehend, more reasonable to scale when needed
  • smaller build sizes — cheaper infrastructure and better horizontal scaling

These benefits are balanced by fundamental trade-offs:

  • the system becomes distributed — network delays and partitions become ordinary and must be designed for
  • the system becomes asynchronous — integration and end-to-end testing becomes significantly harder
  • events are now processed by service chains — bugs become harder to trace, reproduce, and reason about
  • wrong service boundaries are expensive — errors here lead to numerous inter-service dependencies, spawning a “distributed monolith”, combining cons of both approaches while bringing few benefits

Microservices shift complexity from code size to communication structure, forcing boundaries between business logic and supporting infrastructure.

Problems of Monoliths in Growing Projects | Microservice Architecture — Ep. 10

When a project grows successfully, with time amount of code grows, and more devs are hired. This often leads to:

  • a bloated codebase — harder for new engineers to understand, slower to change
  • tight coupling between many modules — releases require more coordination and happen less frequently
  • growing coordination overhead — N team members can form up to N² communication paths
  • legacy accumulation — dependencies receive updates, but codebase upgrades are postponed due to fear of breaking changes
  • longer build times
  • increasing hardware requirements for all environments

This is a time when teams start thinking about splitting the system into smaller, independently evolving parts.

Would you name the approach? :)

It is Right to Start with a Monolith | Microservice Architecture — Ep. 9

Microservices have many benefits — but they are not a default choice for greenfield projects.

Monoliths have strong advantages that service-oriented approaches cannot offer:

  • the whole app is deployed as a single unit: if it compiles, cross-module integration is likely correct
  • development and refactoring are simpler: the compiler helps to track if changes are complete
  • cross-module calls are synchronous or close to synchronous, taking less than 1-10 milliseconds
  • testing is easier, with clear targets of integration and e2e tests
  • debugging is simpler: spin up the app locally, set breakpoints, attach a profiler — and you have the whole system for inspection

When monoliths are OK:

  • small projects
  • small teams
  • unclear domain
  • unclear scaling requirements

...basically, in most new projects.

Earlier Ctrl + ↓