Monolith First, Microservices When Needed
The most common architectural mistake in SaaS platforms is premature decomposition into microservices. A team of 10 engineers managing 30 microservices spends more time on inter-service communication, deployment coordination, and distributed debugging than on delivering features. The pragmatic approach is to start with a well-structured monolith with clear module boundaries, and extract services only when specific scaling, deployment, or team autonomy requirements demand it.
TaptiPM began as a modular monolith with six bounded contexts: Projects (sprints, stories, tasks, bugs), HRMS (employees, leave, payroll, performance), Finance (expenses, invoices, budgets, tax), CRM (clients, deals, pipeline, feature requests), AI (estimation, analytics, recommendations), and Platform (auth, tenancy, notifications, audit). Each context has its own data models, business logic, and API surface — but they all deploy as a single unit. This architecture delivers the development simplicity of a monolith with the organizational clarity of microservices.
Service Boundary Definition
When extraction becomes necessary, the service boundary should follow the bounded context — not the database table. A "User Service" that manages the users table is not a useful boundary because users are referenced by every other module. A "HRMS Service" that owns the complete employee lifecycle (profile, leave, payroll, performance) is a meaningful boundary because it encapsulates a coherent domain with clear contracts to other modules.
The litmus test for a service boundary is: can this service be developed, deployed, and scaled independently without coordinating with other teams? If extracting a service still requires synchronized deployments with three other services because of shared database tables or tight API coupling, the extraction creates operational overhead without delivering independence. TaptiPM defines service contracts through versioned API schemas and event contracts, ensuring that consumers and producers can evolve independently.
Event-Driven Communication
Microservices should communicate through events, not direct API calls, for cross-cutting concerns. When an employee submits a leave request (HRMS domain), the sprint capacity calculation (Projects domain) needs to update. If Projects calls HRMS's API to check leave status on every capacity calculation, it creates tight coupling and a cascade failure risk. Instead, HRMS publishes a "LeaveRequestApproved" event, and Projects subscribes to update capacity asynchronously.
TaptiPM's event bus processes over 60 event types across modules. Events are schema-versioned (v1, v2) with backward compatibility guarantees. Consumers that need the latest event schema upgrade at their own pace while continuing to process older schema versions. The event store retains events for 90 days, enabling consumers to replay events after bug fixes or new service deployments. This eventual consistency model sacrifices real-time synchronization for resilience — sprint capacity might be 30 seconds behind the latest leave approval, but the system never fails because HRMS is temporarily unavailable.
Data Ownership and the Database-Per-Service Pattern
The strictest microservices principle is that each service owns its data exclusively — no service should directly access another service's database. This ensures that internal data schema changes do not break other services. However, this principle creates real challenges for features that need to join data across services: a project profitability report needs data from Projects (effort), HRMS (labor cost), and Finance (revenue).
TaptiPM solves cross-domain queries with a read-optimized aggregation layer. Each service publishes state change events that a Query Service consumes to build denormalized views optimized for cross-domain queries. The project profitability view combines effort data from Projects, cost rates from HRMS, and revenue from Finance into a pre-computed materialized view. Queries against this view are fast (no cross-service joins at query time) and resilient (the view serves stale data during service outages rather than failing entirely). This CQRS (Command Query Responsibility Segregation) pattern is the pragmatic answer to cross-domain data access in microservices.
- Start with a modular monolith and extract services only when scaling or autonomy demands it
- Service boundaries should follow bounded contexts, not database tables
- Event-driven communication with schema versioning ensures loose coupling and independent evolution
- Database-per-service with CQRS aggregation layers solves cross-domain query requirements
- The litmus test for extraction: can this service be developed, deployed, and scaled independently?