-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
309 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
public |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
75 changes: 75 additions & 0 deletions
75
content/posts/microservice-architecture-find-the-right-size.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
+++ | ||
title = 'How to determine the right microservice size' | ||
date = 2021-11-12T15:25:11+02:00 | ||
draft = true | ||
+++ | ||
|
||
## Balancing the size of Microservices | ||
|
||
One of the most common challenges developers and architects face when designing a microservice architecture is determining the optimal size of each service. How "micro" should a microservice really be? In this article, we explore the various factors—acting as driving forces—that influence decisions regarding service size. | ||
<!--more--> | ||
|
||
> This post is the result of my experience as a staff developer and my study of how the software industry addresses well-known software architecture problems (specifically, the book [Software Architecture the hard parts](https://www.amazon.com/Software-Architecture-Trade-Off-Distributed-Architectures/dp/1492086894/)). | ||
|
||
Designing microservice-based systems involves a constant balancing act, as architectural choices are influenced by evolving requirements, performance considerations, and security concerns. Rather than making static decisions, architects must continuously re-evaluate and adjust their designs to adapt to these factors. | ||
|
||
These factors can be divided into the following categories | ||
|
||
1. Technical factos | ||
1. Performance & Scalability | ||
2. Security (https://www.fedramp.gov/, [SOC2](https://www.akamai.com/glossary/what-is-soc2) ) | ||
2. Domain factors | ||
1. Code Change Frequency | ||
2. Multi-purpose services | ||
3. Organizational | ||
1. Over fragmentation | ||
2. Optimize for development process | ||
|
||
## Technical factors | ||
|
||
**Performance and Scalability Needs** | ||
|
||
Diverging performance requirements can also drive the decision to split services. When a single service has components with drastically different performance demands, separating them into distinct microservices allows you to fine-tune each one independently. | ||
|
||
For instance, if a credit card payment component receives 1000x more traffic than other payment providers, scaling the entire payment service together might lead to inefficiencies. Instead, splitting the payment service into separate microservices—one for each provider—enables you to scale each independently based on actual traffic patterns, ensuring more efficient use of resources. | ||
|
||
**Security Requirements** | ||
|
||
Security is another critical factor when deciding whether to split services. Services that handle sensitive data, such as payment details or personally identifiable information (PII), should be isolated from those with lower security requirements to reduce potential vulnerabilities. | ||
|
||
Imagine an e-commerce platform where a customer service handles both user preferences (such as theme settings or notification options) and sensitive payment information. If both functionalities are housed in the same service, the attack surface is broader, and the risk of data leakage increases. By splitting these services, you can apply different security protocols, such as more stringent access controls or encryption, to the payment service while maintaining simpler security for non-sensitive data. | ||
|
||
## Domain factors | ||
|
||
**Multi-Purpose Services** | ||
|
||
One of the key indicators that a service may need to be split is when it serves more than one purpose or encompasses too many distinct responsibilities. In microservice design, it's best to align services with single, well-defined bounded contexts. When a service straddles multiple contexts, it becomes more difficult to maintain, test, and scale independently. | ||
|
||
For example, if a service exposes numerous REST API endpoints that serve different subdomains of your application, this is a strong signal that it may be too large and complex. Splitting it into more focused, context-driven services can improve maintainability and scalability. By clearly defining each subdomain, you can ensure that each service remains manageable and performs optimally. | ||
|
||
**Code Change Frequency** | ||
|
||
Another factor that justifies splitting a service is when certain parts of the codebase change far more frequently than others. Services with uneven change frequencies are harder to manage, as constant updates in one section can introduce risks across the entire service. | ||
|
||
Consider a payment service that handles multiple providers—PayPal, credit card, and a "buy now, pay later" option. The latter might be a new payment method, and its APIs are evolving rapidly. In this scenario, separating each provider into distinct microservices could minimize deployment risks, isolate changes to the "buy now, pay later" service, and reduce the testing surface area. By isolating frequently changing components, you can achieve greater stability in the rest of your system. | ||
|
||
|
||
### Organizational factors | ||
|
||
**Over-Fragmentation** | ||
|
||
Splitting services into smaller ones to have more focused components has its advantages, there are also forces that drive the opposite direction, merging services. The goal is always to avoid the trap of excessive microservice fragmentation, which can introduce complexity and inefficiency. | ||
One of the main risks in a microservice architecture is creating too many services, leading to what is often referred to as "over-fragmentation." When services are too fine grained, communication overhead between services increases, and managing the entire system becomes more complex. This can result in higher latency, difficulties with deployment, and increased operational costs. | ||
|
||
Merging services may become necessary when you find that the overhead of managing multiple microservices outweighs the benefits. For example, if two services are tightly coupled and need to communicate frequently, merging them into a single service could reduce the complexity of inter-service communication and improve performance. | ||
|
||
**Optimize for development process** | ||
|
||
Another reason to merge services is to simplify the development and testing process. If multiple microservices are contributing to the same feature and require coordinated releases, it may be more efficient to combine them into a single unit. This can reduce the coordination effort between teams, simplify CI/CD pipelines, and lead to faster release cycles. | ||
|
||
### Conclusion | ||
|
||
Finding the right balance in microservice size is about managing competing forces. On one hand, you want to split services to improve scalability, maintainability, and security. On the other hand, you must avoid over-fragmenting the architecture, which can introduce unnecessary complexity and operational challenges. | ||
|
||
Ultimately, the decision on microservice size should be guided by your application’s specific needs, and as these needs evolve, so too should your architecture. Constantly reevaluating the size and boundaries of your services is key to maintaining an efficient, secure, and scalable microservice ecosystem. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
+++ | ||
title = 'Deliver notifications in the user browser' | ||
date = 2021-11-12T15:25:11+02:00 | ||
draft = false | ||
+++ | ||
|
||
Delivering real-time notifications in the user browser makes your application way more engaging and let the user quickly react to the events happening in the product.<!--more--> | ||
|
||
-------------------------------- | ||
|
||
**This story starts in a meeting with your team** | ||
|
||
You currently work for a successful e-commerce company and you’re in the middle of a grooming meeting when the product owner asks you to implement the invoice PDF generation | ||
|
||
You know that invoice rendering is a very long-running job, it involves several API calls to fetch all the required data. | ||
|
||
The invoice generation is something like: | ||
|
||
```go | ||
type Invoice struct { | ||
model.User | ||
model.Order | ||
model.Payment | ||
} | ||
|
||
g, ctx := errgroup.WithContext(ctx) | ||
invoice := Invoice{} | ||
|
||
g.Go(func() error { | ||
user, err := userService.Get("user-id") | ||
if err == nil { | ||
invoice.User = user | ||
} | ||
return err | ||
}) | ||
|
||
g.Go(func() error { | ||
order, err := orderService.Get("order-id") | ||
if err == nil { | ||
invoice.Order = order | ||
} | ||
return err | ||
}) | ||
|
||
g.Go(func() error { | ||
payment, err := paymentService.Get("payment-id") | ||
if err == nil { | ||
invoice.Payment = payment | ||
} | ||
return err | ||
}) | ||
|
||
if err := g.Wait(); err != nil { | ||
return nil, err | ||
} | ||
|
||
pdf, err := pdfInvoiceRendered.Render(invoice) | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return pdf, nil | ||
``` | ||
|
||
To generate an invoice the invoice service needs to do several API calls to the external services that own the Payment, User, Order and so on... After that you can render the PDF using the engine of your choice; You know, all that process may take lots of time to complete the job. | ||
|
||
So after a brainstorming, your team decides that it's the right time to implement something to send a notification to the user. 🥳 | ||
|
||
If you want to jump straight to the solution go to [Notify]({{< ref "sse-events-go.md#notify" >}}). | ||
|
||
### Sending notifications to the browser | ||
|
||
There are several ways to implement a notification system for a web application. The easy way is to go through a [polling](https://en.m.wikipedia.org/wiki/Polling_(computer_science)) strategy. | ||
Polling is not beautiful nor efficient because you need to get if there are new notifications at regular intervals and even worse if you have a lot of users you flood your server with huge traffic. | ||
Despite of all the cons, a polling strategy can be good for small products with relatively low traffic. | ||
|
||
**Polling** | ||
|
||
- it's easy to implement | ||
- do not require a persistent connection | ||
|
||
## A more sophisticated solution | ||
|
||
If you’re still reading this post maybe you’re interested in a more sophisticated solution than the polling one. So we'll go deeper into the rabbit hole. | ||
|
||
### Persistent connection | ||
|
||
One of the most significant features that makes a polling implementation simple is that it do not rely on a persistent connection between the client and the server. | ||
Basically, you just need to implement a `GET /notifications` API and let the client call it to fetch new notifications. | ||
The state is preserved in the server where we've hundreds of possibilities to store the notifcations; we can use a database, a cache, the filesystem, or whatever system we want. | ||
|
||
On the other hand, if we don't want to flood the server with hundreds of requests to ask for notifications we need to set up a stable communication channel between the client and the server. Talking about HTTP this channel could be a persistent connection to be used to send messages to the client. | ||
|
||
```bash | ||
➜ ~ curl -v --keepalive http://localhost:3000/open?channel=test | ||
* Trying ::1... | ||
* TCP_NODELAY set | ||
* Connected to localhost (::1) port 3000 (#0) | ||
> GET /open?channel=test HTTP/1.1 | ||
> Host: localhost:3000 | ||
> User-Agent: curl/7.64.1 | ||
> Accept: */* | ||
> | ||
< HTTP/1.1 200 OK | ||
< Cache-Control: no-cache | ||
< Connection: keep-alive | ||
< Content-Type: text/event-stream | ||
< Date: Fri, 12 Nov 2021 15:52:22 GMT | ||
< Transfer-Encoding: chunked | ||
``` | ||
The [HTTP / 1.1](https://en.wikipedia.org/wiki/HTTP_persistent_connection) version of the protocol supports the persistent connections, basically every connection is treated as persistent unless the client (or the server) sends a `Connection: close` header. | ||
This can be a very useful approach to keep the number of newly opened connection under control, even more so we have thousands of clients that need to receive notifications and constantly ping the server to ask for them. | ||
### Server Sent Events - SSE | ||
Beside the classic HTTP request <-> response flow that allows the client to send a request to the server we also have the server sent events in our swiss army knife. | ||
**Server sent events** allows the server to push new data to the browser as long as they are ready. | ||
### Implementing in golang | ||
Thanks to the powerful stdlib it's very easy to implement SSE in golang, you can write data over the response writer and flush it to send them to the client. | ||
```go | ||
func(w http.ResponseWriter, r *http.Request) { | ||
w.Header().Set("Access-Control-Allow-Origin", "*") | ||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type") | ||
w.Header().Set("Content-Type", "text/event-stream") | ||
w.Header().Set("Cache-Control", "no-cache") | ||
w.Header().Set("Connection", "keep-alive") | ||
if f, ok := w.(http.Flusher); ok { | ||
f.Flush() | ||
} | ||
go func() { | ||
for { | ||
time.Sleep(time.Second) | ||
fmt.Fprint(w, "event: test\n") | ||
fmt.Fprint(w, "data: {\"data\": 1}\n\n") | ||
// Flush the write to actually send data to the client | ||
if f, ok := w.(http.Flusher); ok { | ||
f.Flush() | ||
} | ||
} | ||
}() | ||
<-r.Context().Done() | ||
} | ||
``` | ||
But in a real-world scenario that snippet could be not enough, you may need a more sophisticated solution for a cloud-native app that requires: | ||
- horizontal scaling through containers | ||
- release new versions with a [blue/geen deployment](https://en.wikipedia.org/wiki/Blue-green_deployment) | ||
### Notify {#notify} | ||
Notify is a golang library that condensates all these concepts, it allows to **send server events** to the browser with ease. | ||
https://github.com/toretto460/notify is easy to use; it supports Redis as a backend to publish and subscribe to events. | ||
[https://github.com/toretto460/notify](https://github.com/toretto460/notify/blob/main/example/web/main.go) | ||
```go | ||
package main | ||
import ( | ||
"context" | ||
"log" | ||
"net/http" | ||
"time" | ||
"github.com/go-redis/redis/v8" | ||
"github.com/toretto460/notify" | ||
"github.com/toretto460/notify/model" | ||
) | ||
var redisCli *redis.Client | ||
func init() { | ||
redisCli = redis.NewClient(&redis.Options{ | ||
Addr: "localhost:6379", | ||
}) | ||
} | ||
func main() { | ||
// Get the channel factory | ||
chFactory := notify.Redis(redisCli) | ||
// Register the default handler | ||
http.Handle("/listen", notify.DefaultHandler(chFactory)) | ||
// Send some messages | ||
go func() { | ||
channel, _ := chFactory.Get("test-channel-id") | ||
channel.Send( | ||
context.Background(), | ||
model.NewMessage("test-message", []byte(`{"hello": "world"}`)), | ||
) | ||
time.Sleep(time.Second * 10) | ||
}() | ||
log.Print("Starting web server at :3000") | ||
if err := http.ListenAndServe(":3000", nil); err != nil { | ||
log.Fatal(err) | ||
} | ||
} | ||
``` | ||
And then you can listen for messages in the browser | ||
```js | ||
const channel = "test-channel-id" | ||
const source = new EventSource("http://localhost:3000/listen?channel=" + channel) | ||
source.addEventListener('test-message', (event) => { | ||
console.log(event) // => {"hello": "world"} | ||
}) | ||
``` | ||
Disclaimer | ||
As of today 11/11/2021 the notify library is still not used in production. Fill a PR or open a discussion here [https://github.com/toretto460/notify/issues](https://github.com/toretto460/notify/issues) for any issue. | ||
Cover photo by [Adam Solomon](https://unsplash.com/@solomac?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/notification?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) |