⏱️ 50 min read 🎯 Beginner Friendly 🔧 Hands-On Examples 📚 Complete Guide

Core Design Principles

Learn the fundamental principles that guide good microservices design. These principles help create services that are maintainable, scalable, and resilient.

Single Responsibility Principle

Each service should do one thing and do it well - just like how a chef specializes in one cuisine type.

Restaurant Kitchen Analogy

Think of a restaurant kitchen with different stations:

  • Grill Station: Only handles grilled items
  • Salad Station: Only prepares salads
  • Dessert Station: Only makes desserts

Each station (service) has clear responsibility and can work independently.

Core Principles

  • Loose Coupling: Services should be independent, not tightly connected
  • High Cohesion: Related functionality stays together in one service
  • Autonomous: Services can be deployed and scaled independently
  • Clear Boundaries: Each service has well-defined responsibilities
Good vs Bad Service Design
# ❌ BAD: Service doing too much
class UserService:
    def create_user()
    def process_payment()  # Wrong! Payment is different responsibility
    def send_email()       # Wrong! Notification is different responsibility
    def manage_inventory() # Wrong! Inventory is different responsibility

# ✅ GOOD: Each service has single responsibility
class UserService:
    def create_user()
    def update_user()
    def delete_user()

class PaymentService:
    def process_payment()
    def refund_payment()

class NotificationService:
    def send_email()
    def send_sms()

Service Boundaries

How do you know where one service ends and another begins?

  • Business Capability: Group by what the business does (Orders, Payments, Shipping)
  • Data Ownership: Service owns and manages its own data
  • Team Structure: One team can own the entire service

Service Decomposition Examples

Monolith Function Microservice Responsibility
User Management Auth Service Login, logout, token management
Profile Service User data, preferences, settings
Permissions Service Roles, access control, authorization
E-Commerce Catalog Service Product listings, search, inventory
Cart Service Shopping cart operations
Checkout Service Order placement, validation
Payment Service Payment processing, refunds
Node.js - User Service Example
const express = require('express');
const app = express();

// User Service - ONLY manages user profiles
// Does NOT handle authentication or payments

class UserService {
    // Create user profile
    async createProfile(userId, profileData) {
        const profile = {
            userId,
            firstName: profileData.firstName,
            lastName: profileData.lastName,
            email: profileData.email,
            preferences: profileData.preferences || {},
            createdAt: new Date()
        };

        await db.users.insert(profile);
        return profile;
    }

    // Update user profile
    async updateProfile(userId, updates) {
        // Validate user owns this profile
        const profile = await db.users.findOne({ userId });
        if (!profile) {
            throw new Error('Profile not found');
        }

        // Update only profile-related fields
        const updated = {
            ...profile,
            ...updates,
            updatedAt: new Date()
        };

        await db.users.update({ userId }, updated);
        return updated;
    }

    // Get user profile
    async getProfile(userId) {
        return await db.users.findOne({ userId });
    }
}

// REST API endpoints
app.get('/users/:id', async (req, res) => {
    const profile = await userService.getProfile(req.params.id);
    res.json(profile);
});

app.put('/users/:id', async (req, res) => {
    const updated = await userService.updateProfile(
        req.params.id,
        req.body
    );
    res.json(updated);
});

app.listen(3001);
How to Identify Service Boundaries

Ask these questions:

  • Can this functionality change independently?
  • Does it have its own data that other services don't need to directly access?
  • Can one team own this completely?
  • Does it represent a distinct business capability?

If you answer "yes" to most of these, you've found a good service boundary!

Domain-Driven Design (DDD) Fundamentals

DDD helps you define clear service boundaries based on business domains. It's one of the most important concepts for successful microservices architecture.

Bounded Contexts

A bounded context is a clear boundary within which a domain model is valid. Each microservice typically represents one bounded context.

Python - Different Bounded Contexts
# Sales Context - Customer as Buyer
class Customer:
    customer_id: str
    credit_limit: Decimal
    purchase_history: List[Order]
    loyalty_points: int
    payment_methods: List[PaymentMethod]

# Support Context - Customer as Support Case
class Customer:
    customer_id: str
    support_tier: str  # bronze, silver, gold
    open_tickets: List[Ticket]
    satisfaction_score: float
    contact_preferences: dict

# Shipping Context - Customer as Delivery Recipient
class Customer:
    customer_id: str
    shipping_addresses: List[Address]
    delivery_preferences: dict
    delivery_instructions: str

Notice how "Customer" means different things in different contexts. This is natural and correct!

Ubiquitous Language

The team and code should use the same language as the business domain experts.

Domain Ubiquitous Language Technical Translation
E-commerce Cart, Checkout, Order ShoppingCart, PaymentProcess, OrderAggregate
Banking Account, Transaction, Balance BankAccount, MoneyTransfer, AccountBalance
Healthcare Patient, Appointment, Prescription PatientRecord, ScheduledVisit, Medication

Aggregate Patterns

An aggregate is a cluster of domain objects that can be treated as a single unit.

Java - Order Aggregate Example
// Order is the Aggregate Root
public class Order {
    private OrderId id;
    private CustomerId customerId;
    private List orderLines;  // Part of aggregate
    private OrderStatus status;
    private Money totalAmount;

    // Business logic encapsulated
    public void addItem(Product product, int quantity) {
        OrderLine line = new OrderLine(product, quantity);
        this.orderLines.add(line);
        this.recalculateTotal();
    }

    public void placeOrder() {
        if (orderLines.isEmpty()) {
            throw new BusinessException("Cannot place empty order");
        }
        this.status = OrderStatus.PLACED;
        // Emit domain event
        DomainEvents.raise(new OrderPlaced(this.id));
    }

    private void recalculateTotal() {
        this.totalAmount = orderLines.stream()
            .map(OrderLine::getSubtotal)
            .reduce(Money.ZERO, Money::add);
    }
}
Aggregate Rules
  • Reference other aggregates by ID only, not by direct object reference
  • Keep aggregates small - large aggregates cause performance issues
  • One aggregate per transaction - this ensures consistency

Repository Pattern

Repositories provide a collection-like interface for accessing aggregates. They hide the database complexity.

Python - Repository Pattern
from abc import ABC, abstractmethod
from typing import Optional, List

# Repository interface (port)
class OrderRepository(ABC):
    @abstractmethod
    def save(self, order: Order) -> None:
        pass

    @abstractmethod
    def find_by_id(self, order_id: str) -> Optional[Order]:
        pass

    @abstractmethod
    def find_by_customer(self, customer_id: str) -> List[Order]:
        pass

    @abstractmethod
    def delete(self, order_id: str) -> None:
        pass

# Concrete implementation (adapter)
class PostgresOrderRepository(OrderRepository):
    def __init__(self, db_connection):
        self.db = db_connection

    def save(self, order: Order) -> None:
        # Convert domain object to database row
        data = {
            'order_id': order.id,
            'customer_id': order.customer_id,
            'status': order.status.value,
            'total': float(order.total_amount),
            'items': json.dumps([
                {'product_id': item.product_id, 'quantity': item.quantity}
                for item in order.items
            ])
        }
        self.db.execute(
            "INSERT INTO orders VALUES (%s, %s, %s, %s, %s) "
            "ON CONFLICT (order_id) DO UPDATE SET status=%s, total=%s",
            (data['order_id'], data['customer_id'], data['status'],
             data['total'], data['items'], data['status'], data['total'])
        )

    def find_by_id(self, order_id: str) -> Optional[Order]:
        row = self.db.query_one(
            "SELECT * FROM orders WHERE order_id = %s",
            (order_id,)
        )
        if not row:
            return None

        # Reconstruct domain object from database row
        return Order(
            order_id=row['order_id'],
            customer_id=row['customer_id'],
            items=json.loads(row['items']),
            status=OrderStatus(row['status']),
            total_amount=Decimal(row['total'])
        )

    def find_by_customer(self, customer_id: str) -> List[Order]:
        rows = self.db.query(
            "SELECT * FROM orders WHERE customer_id = %s",
            (customer_id,)
        )
        return [self._row_to_order(row) for row in rows]

Domain Events

Events represent something significant that happened in the domain. They enable loose coupling between services.

Java - Domain Events
// Domain Event
public class OrderPlacedEvent {
    private final String orderId;
    private final String customerId;
    private final Money totalAmount;
    private final LocalDateTime occurredAt;

    public OrderPlacedEvent(String orderId, String customerId, Money totalAmount) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.totalAmount = totalAmount;
        this.occurredAt = LocalDateTime.now();
    }
    // Getters...
}

// Aggregate raises events
public class Order {
    private List domainEvents = new ArrayList<>();

    public void placeOrder() {
        // Validate order can be placed
        if (this.items.isEmpty()) {
            throw new BusinessException("Cannot place empty order");
        }

        // Change state
        this.status = OrderStatus.PLACED;

        // Raise domain event
        this.domainEvents.add(
            new OrderPlacedEvent(this.id, this.customerId, this.totalAmount)
        );
    }

    public List getDomainEvents() {
        return Collections.unmodifiableList(domainEvents);
    }

    public void clearDomainEvents() {
        this.domainEvents.clear();
    }
}

// Service publishes events after saving
@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private EventPublisher eventPublisher;

    @Transactional
    public void placeOrder(Order order) {
        // Execute business logic
        order.placeOrder();

        // Save to database
        orderRepository.save(order);

        // Publish events (other services can subscribe)
        order.getDomainEvents().forEach(event -> {
            eventPublisher.publish(event);
        });

        order.clearDomainEvents();
    }
}
Benefits of Domain Events
  • Loose Coupling: Services don't need direct references
  • Audit Trail: Events record what happened in the system
  • Temporal Queries: Replay events to see past states
  • Integration: Other systems can subscribe to events

Context Mapping in Practice

Go - Published Language Pattern
package contracts

// Published Language - Well-defined contract
// Other services integrate using this stable interface

type ProductDTO struct {
    ID          string    `json:"id"`
    Name        string    `json:"name"`
    Description string    `json:"description"`
    Price       float64   `json:"price"`
    Currency    string    `json:"currency"`
    InStock     bool      `json:"in_stock"`
    Categories  []string  `json:"categories"`
}

type ProductAPI interface {
    GetProduct(productID string) (*ProductDTO, error)
    SearchProducts(query string, limit int) ([]ProductDTO, error)
    CheckAvailability(productID string, quantity int) (bool, error)
}

// Internal domain model can be different
type Product struct {
    productID   ProductID
    name        ProductName
    price       Money
    inventory   Inventory
    // ... complex internal structure
}

// Translator converts between internal and published models
type ProductTranslator struct {}

func (t *ProductTranslator) ToDTO(product *Product) *ProductDTO {
    return &ProductDTO{
        ID:          product.productID.String(),
        Name:        product.name.Value(),
        Description: product.GetDescription(),
        Price:       product.price.Amount(),
        Currency:    product.price.Currency(),
        InStock:     product.inventory.IsAvailable(),
        Categories:  product.GetCategoryNames(),
    }
}

DDD Building Blocks

Domain-Driven Design provides tactical patterns that map directly to microservices components.

Entities and Value Objects

Entity: Has identity that persists over time (e.g., User, Order)
Value Object: Defined by its attributes, no identity (e.g., Address, Money)

Python - Entity vs Value Object
# Entity - has identity (ID)
class Order:
    def __init__(self, order_id):
        self.id = order_id  # Identity
        self.items = []
        self.total = 0

    def __eq__(self, other):
        return self.id == other.id  # Compared by ID

# Value Object - no identity
class Money:
    def __init__(self, amount, currency):
        self.amount = amount
        self.currency = currency

    def __eq__(self, other):
        return self.amount == other.amount and self.currency == other.currency

Aggregates and Aggregate Roots

An aggregate is a cluster of entities and value objects treated as a single unit. The aggregate root is the only entry point.

  • Order Aggregate: Order (root) → OrderItems → Product References
  • Customer Aggregate: Customer (root) → Addresses → PaymentMethods

Repositories

Provide an abstraction for data access. One repository per aggregate root.

Domain Services

Business logic that doesn't belong to a single entity (e.g., PricingService, ShippingCalculator).

Domain Events

Something significant that happened in the domain (OrderPlaced, PaymentProcessed, UserRegistered).

Hands-On Implementation

Practical code examples implementing DDD patterns in real microservices.

Node.js User Service with DDD

Node.js - Complete User Service
const express = require('express');
const app = express();
app.use(express.json());

// User Service - manages user lifecycle
const users = new Map();

// Create user
app.post('/users', (req, res) => {
    const { name, email } = req.body;
    const userId = Date.now().toString();

    const user = {
        id: userId,
        name,
        email,
        createdAt: new Date()
    };

    users.set(userId, user);
    res.status(201).json(user);
});

// Get user
app.get('/users/:id', (req, res) => {
    const user = users.get(req.params.id);
    if (!user) {
        return res.status(404).json({ error: 'User not found' });
    }
    res.json(user);
});

app.listen(3000, () => console.log('User service running on port 3000'));

Python Repository Pattern

Python - Repository Implementation
from abc import ABC, abstractmethod
from typing import Optional, List

class OrderRepository(ABC):
    @abstractmethod
    def save(self, order: Order) -> None:
        pass

    @abstractmethod
    def find_by_id(self, order_id: str) -> Optional[Order]:
        pass

    @abstractmethod
    def find_all(self) -> List[Order]:
        pass

class PostgresOrderRepository(OrderRepository):
    def __init__(self, db_connection):
        self.db = db_connection

    def save(self, order: Order) -> None:
        # Convert domain object to database row
        data = {
            'order_id': order.id,
            'customer_id': order.customer_id,
            'status': order.status.value,
            'total': float(order.total_amount),
            'created_at': order.created_at
        }
        self.db.execute(
            "INSERT INTO orders VALUES (:order_id, :customer_id, :status, :total, :created_at)",
            data
        )

    def find_by_id(self, order_id: str) -> Optional[Order]:
        row = self.db.query_one("SELECT * FROM orders WHERE order_id = :id", {'id': order_id})
        if not row:
            return None
        return self._map_to_domain(row)

    def _map_to_domain(self, row) -> Order:
        # Convert database row to domain object
        return Order(
            id=row['order_id'],
            customer_id=row['customer_id'],
            status=OrderStatus(row['status']),
            total_amount=row['total']
        )

Java Domain Events

Java - Domain Events Pattern
// Domain Event
public class OrderPlacedEvent {
    private final String orderId;
    private final String customerId;
    private final BigDecimal totalAmount;
    private final Instant timestamp;

    public OrderPlacedEvent(String orderId, String customerId, BigDecimal totalAmount) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.totalAmount = totalAmount;
        this.timestamp = Instant.now();
    }
}

// Order Aggregate
public class Order {
    private String id;
    private String customerId;
    private List items;
    private OrderStatus status;
    private List domainEvents = new ArrayList<>();

    public void place() {
        this.status = OrderStatus.PLACED;

        // Raise domain event
        domainEvents.add(new OrderPlacedEvent(
            this.id,
            this.customerId,
            calculateTotal()
        ));
    }

    public List getDomainEvents() {
        return Collections.unmodifiableList(domainEvents);
    }
}

Service Boundary Identification Walkthrough

Let's analyze a Social Media Platform and identify service boundaries:

Service Boundary Analysis
# Social Media Platform - Service Decomposition

## Identified Bounded Contexts:

1. **User Management Context**
   - Handles: Registration, authentication, profiles
   - Data: User accounts, credentials, settings
   - Service: User Service

2. **Content Publishing Context**
   - Handles: Creating posts, editing, deleting content
   - Data: Posts, media attachments, drafts
   - Service: Content Service

3. **Social Graph Context**
   - Handles: Friend connections, followers, following
   - Data: Relationships between users
   - Service: Graph Service

4. **Engagement Context**
   - Handles: Likes, comments, shares, reactions
   - Data: Engagement metrics, interaction history
   - Service: Engagement Service

5. **Notification Context**
   - Handles: Sending notifications, preferences
   - Data: Notification queue, delivery status
   - Service: Notification Service

6. **Feed Context**
   - Handles: Generating personalized feeds, ranking
   - Data: Feed cache, user preferences
   - Service: Feed Service

## Context Relationships:
- Content Service publishes events → Engagement Service reacts
- Engagement Service publishes events → Notification Service sends alerts
- Feed Service consumes from Content + Social Graph + Engagement

Try This Analysis

For your own application:

  1. List all major features/capabilities
  2. Group related features together
  3. Identify data ownership boundaries
  4. Draw context relationships
  5. Validate: Can each context be owned by one team?

Practice Exercises

Apply DDD and architecture principles through hands-on exercises.

Exercise 1: Identify Service Boundaries

Objective: Decompose a monolithic e-learning platform into microservices

Scenario: An e-learning platform has these features:

  • User registration, login, profiles
  • Course catalog, course creation, content management
  • Video streaming, progress tracking
  • Quizzes, assignments, grading
  • Payments, subscriptions
  • Certificates generation
  • Discussion forums

Task: Identify 5-7 microservices with clear boundaries

Solution:

Proposed Microservices
1. **User Service**
   - Registration, authentication, profiles
   - Data: User accounts, credentials

2. **Course Catalog Service**
   - Course listings, search, metadata
   - Data: Course information, categories

3. **Content Delivery Service**
   - Video streaming, content access control
   - Data: Media files, access logs

4. **Progress Tracking Service**
   - Learning progress, completion status
   - Data: User progress, watched videos, completed lessons

5. **Assessment Service**
   - Quizzes, assignments, grading
   - Data: Questions, submissions, grades

6. **Payment Service**
   - Payments, subscriptions, billing
   - Data: Transactions, payment methods

7. **Certificate Service**
   - Certificate generation, verification
   - Data: Issued certificates, templates

8. **Community Service**
   - Discussion forums, comments
   - Data: Forum posts, threads, comments

Exercise 2: Implement Repository Pattern

Objective: Create a repository for the Product aggregate

Requirements:

  1. Define a Product entity with id, name, price, inventory
  2. Create a ProductRepository interface
  3. Implement an in-memory version for testing
  4. Methods: save(), findById(), findAll(), delete()

Hint: Use the Order Repository example as a template

Exercise 3: Define Bounded Contexts

Objective: Model different bounded contexts for an e-commerce system

Scenario: In an e-commerce system, "Product" means different things in different contexts:

  • Catalog Context: Product as searchable item
  • Inventory Context: Product as stock item
  • Pricing Context: Product as priceable item

Task: Define the Product model for each context with appropriate attributes

Solution:

Python - Different Product Models
# Catalog Context
class Product:
    product_id: str
    name: str
    description: str
    category: str
    tags: List[str]
    images: List[str]
    search_keywords: List[str]

# Inventory Context
class Product:
    product_id: str
    sku: str
    quantity_on_hand: int
    quantity_reserved: int
    reorder_point: int
    warehouse_location: str

# Pricing Context
class Product:
    product_id: str
    base_price: Decimal
    currency: str
    discount_rules: List[DiscountRule]
    pricing_tier: str
    tax_category: str

Strategic DDD and Service Decomposition

Advanced patterns for complex domains and large-scale microservices architectures.

Context Mapping Patterns

Define relationships between bounded contexts:

Pattern Relationship When to Use
Shared Kernel Two contexts share common code Very closely related domains, same team
Customer-Supplier Upstream provides, downstream consumes Clear provider-consumer relationship
Conformist Downstream conforms to upstream model Upstream is external/unchangeable
Anti-Corruption Layer Translation layer protects your model Integrate with legacy or external systems
Published Language Well-documented common language Public APIs, integration points
Python - Anti-Corruption Layer Pattern
# Your Clean Domain Model
class Order:
    def __init__(self, order_id, customer, items):
        self.order_id = order_id
        self.customer = customer
        self.items = items

# Legacy System has messy model
class LegacyOrderData:
    # Fields use different names, structures
    ORD_NUM: str
    CUST_CODE: str
    LINE_ITEMS: str  # Comma-separated!

# Anti-Corruption Layer translates
class LegacyOrderAdapter:
    def to_domain_model(self, legacy_order: LegacyOrderData) -> Order:
        # Translate legacy to clean model
        customer = self.customer_service.get_by_code(
            legacy_order.CUST_CODE
        )
        items = self._parse_line_items(legacy_order.LINE_ITEMS)

        return Order(
            order_id=legacy_order.ORD_NUM,
            customer=customer,
            items=items
        )

    def to_legacy_model(self, order: Order) -> LegacyOrderData:
        # Translate clean model to legacy
        return LegacyOrderData(
            ORD_NUM=order.order_id,
            CUST_CODE=order.customer.code,
            LINE_ITEMS=",".join([i.sku for i in order.items])
        )

Event Storming for Service Boundaries

Event Storming is a workshop technique to discover domain events and service boundaries:

  1. Identify Domain Events: What happens in your business? (OrderPlaced, PaymentProcessed, ItemShipped)
  2. Group Related Events: Events that happen together often belong in same service
  3. Find Aggregates: What entities generate these events?
  4. Define Bounded Contexts: Draw boundaries around related aggregates
  5. Map Context Relationships: How do contexts interact?

Subdomain Classification

Not all parts of your system are equally important. DDD categorizes subdomains to guide investment:

Subdomain Type Definition Strategy Examples
Core Domain Your competitive advantage, what makes you unique Build in-house, invest heavily, best engineers Amazon's recommendation engine, Netflix's streaming algorithm
Supporting Domain Necessary but not differentiating Build in-house, but simpler implementations Inventory management, order processing
Generic Domain Common to all businesses, not unique Buy off-the-shelf or use open-source Authentication, email, payment processing
E-Commerce Subdomain Classification
# CORE DOMAIN - Competitive Advantage
Product Recommendation Service
├── ML-based personalization
├── Real-time behavior tracking
├── A/B testing framework
└── Custom algorithms (proprietary)

# SUPPORTING DOMAIN - Necessary but standard
Inventory Management Service
├── Stock tracking
├── Warehouse integration
├── Reorder point calculations
└── Standard business logic

# GENERIC DOMAIN - Buy or use existing solutions
Authentication Service → Use Auth0 or Okta
Email Service → Use SendGrid
Payment Processing → Use Stripe
Logging → Use Datadog or ELK Stack
Investment Decision Framework

Core Domain: Invest 60-70% of engineering time

Supporting Domain: Invest 20-30% of engineering time

Generic Domain: Invest 10% or less - use existing solutions

Airbnb's Domain Modeling

Airbnb decomposed their monolith using DDD principles:

Bounded Contexts:

  • Listing Context: Property listings, availability, amenities
  • Booking Context: Reservations, cancellations, modifications
  • Pricing Context: Dynamic pricing, smart pricing algorithms
  • Messaging Context: Guest-host communication
  • Reviews Context: Ratings, written reviews, trust signals
  • Payment Context: Transactions, payouts, currencies

Subdomain Classification:

Type Domains
Core Pricing algorithms, Trust & Safety, Search & Discovery
Supporting Booking, Messaging, Listing management
Generic Authentication (OAuth), Payments (Stripe), Email (SendGrid)

Result: Each context became an independent microservice owned by a dedicated team. Clear boundaries enabled parallel development and independent deployment.

LinkedIn's Service Boundaries

LinkedIn evolved from a monolithic Rails app to 500+ microservices using DDD.

Major Bounded Contexts:

  • Identity Context: User profiles, connections, network graph (1B+ members)
  • Feed Context: News feed, content distribution, engagement
  • Messaging Context: InMail, chat, notifications
  • Jobs Context: Job postings, applications, matching algorithm
  • Learning Context: LinkedIn Learning content and progress
  • Sales Navigator Context: B2B sales tools and leads

Key Architectural Decisions:

  • Data Partitioning: Sharded by member ID across 1000s of database shards
  • Caching Strategy: Memcached + local caching for profile data
  • Event Streaming: Kafka for real-time data propagation (4+ trillion events/day)
  • API Gateway: "Rest.li" framework for consistent API design

Migration Lessons:

  1. Started with extracting "Identity" service (most critical)
  2. Built comprehensive monitoring before decomposing
  3. Used strangler fig pattern over 3+ years
  4. Maintained backward compatibility throughout migration

Outcome: 100+ development teams working independently, 1000s of deployments per day, 99.99% uptime

Shopify's Service Decomposition

Shopify serves 2M+ merchants with a microservices architecture designed for multi-tenancy.

Core Service Boundaries:

Service Responsibility Scale
Shop Service Store configuration, branding, domains 2M+ shops
Product Service Product catalog, variants, inventory 100M+ products
Order Service Order processing, fulfillment 10,000+ orders/minute peak
Checkout Service Cart, payment processing $200B+ GMV annually
Shipping Service Shipping rates, labels, tracking 100+ carrier integrations

DDD Principles Applied:

  • Bounded Contexts: Services aligned with merchant mental models
  • Ubiquitous Language: API terms match merchant terminology (not technical jargon)
  • Anti-Corruption Layers: Protect core from 1000s of third-party app integrations
  • Domain Events: Event-driven architecture for webhook system (billions of events)

Multi-Tenancy Strategy:

  • Each service handles all shops (not one service per shop)
  • Data partitioned by shop_id for isolation
  • Resource limits enforced per shop to prevent noisy neighbors
  • Separate "Plus" tier services for enterprise customers

Key Insight: Service boundaries follow merchant workflows, not technical layers. A merchant thinks about "Products" and "Orders", not "database" and "API".

Service Granularity Decision Framework

Service is Right Size When:
  • Can be owned by one small team (2-pizza team)
  • Has single, clear business purpose
  • Can be deployed independently
  • Data can be contained within service
  • Failure doesn't require cascading rollbacks
Service is Too Small When:
  • Excessive network chattiness between services
  • Difficult to understand transaction flow
  • More time spent on coordination than development
  • Shared data access patterns across multiple services

Practical Service Boundary Identification

Let's walk through a real example of identifying service boundaries for a social media platform:

Social Media Platform - Boundary Analysis
DOMAIN: Social Media Platform
========================================

STEP 1: Identify Core Business Capabilities
-------------------------------------------
- User Management (registration, profiles, authentication)
- Content Creation (posts, photos, videos)
- Social Graph (followers, friends, connections)
- Feed Generation (timeline, discovery)
- Notifications (alerts, messages)
- Analytics (engagement metrics, insights)
- Advertising (ad serving, targeting)

STEP 2: Apply Bounded Context Analysis
---------------------------------------
Context: USER IDENTITY
├── Entities: User, Profile, Credentials
├── Data: email, password, name, bio
├── Operations: register(), login(), updateProfile()
└── Team Ownership: Identity Team (5 engineers)

Context: CONTENT
├── Entities: Post, Photo, Video, Comment
├── Data: content_text, media_files, metadata
├── Operations: create(), edit(), delete(), moderate()
└── Team Ownership: Content Team (8 engineers)

Context: SOCIAL GRAPH
├── Entities: Relationship, Follow, Friend
├── Data: follower_id, following_id, relationship_type
├── Operations: follow(), unfollow(), suggestFriends()
└── Team Ownership: Graph Team (6 engineers)

Context: FEED
├── Entities: FeedItem, Timeline
├── Data: Aggregated view of content + graph
├── Operations: generateFeed(), rank(), filter()
└── Team Ownership: Feed Team (10 engineers - complex algorithms)

Context: NOTIFICATIONS
├── Entities: Notification, Alert
├── Data: recipient_id, type, content, read_status
├── Operations: send(), markAsRead(), getUnread()
└── Team Ownership: Notifications Team (4 engineers)

STEP 3: Define Service Boundaries
----------------------------------
✅ GOOD SERVICE BOUNDARIES (follow contexts):

1. User Service
   - Auth & Profile management
   - Database: users, credentials, profiles
   - API: POST /users, GET /users/:id, PUT /users/:id/profile

2. Content Service
   - Post/Photo/Video creation and storage
   - Database: posts, media, comments
   - API: POST /posts, GET /posts/:id, DELETE /posts/:id

3. Social Graph Service
   - Relationship management
   - Database: relationships (optimized graph database)
   - API: POST /users/:id/follow, GET /users/:id/followers

4. Feed Service
   - Timeline generation (reads from Content + Graph)
   - Database: cached feeds
   - API: GET /users/:id/feed

5. Notification Service
   - Alert delivery across channels
   - Database: notifications
   - API: POST /notifications, GET /users/:id/notifications

STEP 4: Validate Boundaries
---------------------------
✓ Each service has single responsibility
✓ Services can deploy independently
✓ One team can own each service
✓ Clear data ownership
✓ Minimal coupling between services

❌ BAD BOUNDARY EXAMPLES:
X  "Database Service" - too generic, not a business capability
X  "CRUD Service" - technical, not business-focused
X  "Helper Service" - vague responsibility
X  Combining User + Content - violates single responsibility

Service Decomposition Approaches Comparison

Approach How It Works Pros Cons Best For
Domain-Driven Design Model business domains, identify bounded contexts • Aligns with business
• Natural boundaries
• Long-term maintainability
• Requires domain expertise
• Steeper learning curve
• Time intensive
Complex business domains, long-term projects
Business Capability Decompose by what the business does • Easy to understand
• Stable over time
• Aligns with org structure
• May miss technical boundaries
• Can be too coarse-grained
Enterprise applications, established businesses
Strangler Fig Gradually extract services from monolith • Low risk
• Incremental
• Learn as you go
• Slow process
• Temporary duplication
• Complex during transition
Migrating from legacy monolith
Technical Seams Split by technical layers (frontend, backend, data) • Quick to implement
• Familiar to developers
• Creates distributed monolith
• Tight coupling
• Not recommended
❌ Avoid this approach
Data Flow Follow data ownership and access patterns • Clear data boundaries
• Performance focused
• May not align with business
• Can change with requirements
Data-intensive applications
Python - Boundary Detection Helper
class ServiceBoundaryAnalyzer:
    """Helper to evaluate if a proposed service boundary is good"""

    def __init__(self, service_name, responsibilities, data_owned, dependencies):
        self.name = service_name
        self.responsibilities = responsibilities
        self.data = data_owned
        self.dependencies = dependencies

    def analyze(self):
        """Run all validation checks"""
        results = {
            'single_responsibility': self._check_single_responsibility(),
            'data_ownership': self._check_data_ownership(),
            'coupling': self._check_coupling(),
            'team_ownership': self._check_team_ownership(),
            'deployment_independence': self._check_deployment()
        }

        score = sum(results.values())
        max_score = len(results)

        return {
            'service': self.name,
            'score': f"{score}/{max_score}",
            'details': results,
            'recommendation': self._get_recommendation(score, max_score)
        }

    def _check_single_responsibility(self):
        """Service should have one clear purpose"""
        if len(self.responsibilities) <= 3 and \
           all(r.startswith(self.name.split('Service')[0].lower())
               for r in self.responsibilities):
            return True
        return False

    def _check_data_ownership(self):
        """Service should own its data"""
        return len(self.data) > 0  # Has its own data

    def _check_coupling(self):
        """Minimal dependencies on other services"""
        return len(self.dependencies) < 5

    def _check_team_ownership(self):
        """Can one team (5-10 people) own this?"""
        # Heuristic: <5 responsibilities = manageable
        return len(self.responsibilities) < 5

    def _check_deployment(self):
        """Can deploy without touching other services?"""
        # If no shared database, can deploy independently
        return 'shared_db' not in self.dependencies

    def _get_recommendation(self, score, max_score):
        ratio = score / max_score
        if ratio >= 0.8:
            return "✅ Good service boundary"
        elif ratio >= 0.6:
            return "⚠️  Acceptable, but could improve"
        else:
            return "❌ Reconsider this boundary"


# Example usage
order_service = ServiceBoundaryAnalyzer(
    service_name="OrderService",
    responsibilities=[
        "create_order",
        "update_order_status",
        "get_order_details"
    ],
    data_owned=["orders", "order_items"],
    dependencies=["ProductService", "PaymentService"]
)

print(order_service.analyze())
# Output: {'service': 'OrderService', 'score': '5/5',
#          'recommendation': '✅ Good service boundary'}

# Bad example
helper_service = ServiceBoundaryAnalyzer(
    service_name="HelperService",
    responsibilities=[
        "send_email",
        "log_events",
        "calculate_tax",
        "generate_pdf",
        "validate_address"
    ],
    data_owned=[],
    dependencies=["UserService", "OrderService", "ProductService",
                  "EmailService", "shared_db"]
)

print(helper_service.analyze())
# Output: {'service': 'HelperService', 'score': '0/5',
#          'recommendation': '❌ Reconsider this boundary'}
Final Checklist: Is This a Good Service?

Before finalizing a service boundary, ask:

  1. Business Alignment: Does it map to a clear business capability?
  2. Single Responsibility: Can you describe what it does in one sentence?
  3. Data Ownership: Does it own and manage its own data?
  4. Team Ownership: Can one small team (5-10 people) own it completely?
  5. Independent Deployment: Can you deploy it without coordinating with other teams?
  6. Clear Interface: Does it expose a well-defined API?
  7. Loose Coupling: Depends on <5 other services?
  8. Bounded Context: Models within have consistent meaning?

If you answered "yes" to 7-8 questions: ✅ Excellent service boundary

If you answered "yes" to 5-6 questions: ⚠️ Acceptable, but refine

If you answered "yes" to <5 questions: ❌ Rethink this boundary