The Architectural Pendulum
For years, the industry swung between extremes: monolith is dead → everything must be microservices → actually, monoliths are fine again.
After building systems for Indonesian companies ranging from 5-person startups to 500-person enterprises, here's my honest take: the right architecture depends on your team size, complexity, and business stage.
Decision Framework
Here's the framework I use when advising clients:
Choose Monolith When:
- Team < 5 developers — Microservices require DevOps expertise that small teams don't have
- Early product stage — You're still finding product-market fit. Speed of iteration matters most.
- Simple domain — The business logic is straightforward. No complex inter-service communication needed.
- Limited budget — Each microservice needs its own CI/CD, monitoring, and potentially its own database
Choose Microservices When:
-
li>Team > 10 developers — Multiple teams need to work independently without blocking each other
- Clear domain boundaries — You have well-defined bounded contexts (orders, payments, inventory, etc.)
- Different scaling needs — Payment processing needs to scale differently than user management
- Technology diversity — Different services genuinely need different tech stacks
- High availability requirements — One service failing shouldn't bring down the entire system
The Monolith That Scales
Here's the thing nobody tells you: a well-structured monolith can handle 90% of what most companies need.
I built a monolithic Go application that handles 50K requests/second for an e-commerce platform with 2M+ users. The key? Clean internal architecture.
// Modular monolith structure
type App struct {
// Internal modules
Products *product.Module
Orders *order.Module
Payments *payment.Module
Users *user.Module
Inventory *inventory.Module
// Shared infrastructure
DB *sqlx.DB
Cache *redis.Client
AI *ai.Service
Queue *message.Queue
}
// Modules communicate through internal interfaces, not HTTP
type OrderModule struct {
product ProductInterface // Depends on interface, not concrete type
payment PaymentInterface
inventory InventoryInterface
}
func (m *OrderModule) CreateOrder(ctx context.Context, req CreateOrderRequest) (*Order, error) {
// Step 1: Validate inventory
available, err := m.inventory.CheckAvailability(ctx, req.Items)
if err != nil {
return nil, err
}
// Step 2: Create order
order := newOrder(req)
// Step 3: Process payment
payment, err := m.payment.Charge(ctx, order.Total)
if err != nil {
return nil, err
}
// Step 4: Reduce inventory
m.inventory.Reserve(ctx, req.Items)
return order, nil
}
When to Migrate from Monolith to Microservices
Don't migrate proactively. Migrate when you hit specific pain points:
- Deployment bottlenecks: Teams can't deploy independently. One team's change requires everyone to coordinate.
- Scaling asymmetry: One module needs 10x more resources than others, but you can't scale it independently.
- Technology constraints: A specific component needs a different programming language or database.
- Team autonomy: Multiple teams stepping on each other's code constantly.
The Strangler Fig Pattern
If you do need to migrate, use the Strangler Fig pattern — not a big-bang rewrite:
// Phase 1: Extract one service at a time
// Start with the most independent module
// Step 1: Create the new microservice
func NewPaymentService(config Config) *fiber.App {
app := fiber.New()
// New, independent database
db := initDB(config.DatabaseURL)
// API endpoints
app.Post("/payments/charge", handlers.Charge(db))
app.Post("/payments/refund", handlers.Refund(db))
app.Get("/payments/:id", handlers.GetPayment(db))
return app
}
// Step 2: Add API gateway routing
func setupGateway(monolithURL, paymentServiceURL string) {
router := httputil.NewSingleHostReverseProxy(monolithURL)
paymentRouter := httputil.NewSingleHostReverseProxy(paymentServiceURL)
mux := http.NewServeMux()
// Route payment endpoints to new service
mux.HandleFunc("/api/v1/payments/", func(w http.ResponseWriter, r *http.Request) {
paymentRouter.ServeHTTP(w, r)
})
// Everything else goes to monolith
mux.Handle("/", router)
}
// Step 3: Migrate data incrementally
// Step 4: Switch traffic gradually (canary deployment)
// Step 5: Remove old code from monolith
The Hybrid Approach (What Most Indonesian Companies Actually Need)
Most successful Indonesian companies I work with use a modular monolith with strategic microservice extraction:
- Core business logic: Monolith (fast iteration, simple deployment)
- Payment processing: Separate service (different scaling, compliance requirements)
- Notification system: Separate service (WhatsApp, email, SMS — different failure modes)
- AI services: Separate service (different resource needs, GPU optional)
Cost Comparison
| Factor | Monolith | Microservices |
|---|---|---|
| Infrastructure cost | Rp 500K-2M/month | Rp 3-10M/month |
| DevOps complexity | Low | High |
| Deployment speed | Fast (single binary) | Medium (multiple services) |
| Team needed | 1-3 developers | 5-10+ developers |
| Time to market | Faster | Slower initially |
| Scalability | Vertical only | Horizontal + vertical |
The Bottom Line
Start with a monolith. Extract services when you have clear reasons and the team to support it. The best architecture is the one your team can build, maintain, and iterate on — not the one that looks best on an architecture diagram.