Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 3

Defining Core Feature Boundaries

Recommended resources

Welcome back. In our last lesson, we established the high-level architecture for our SaaS platform, identifying the major components from the frontend client to the backend services. We sketched out a service layer containing services for auth, tenants, projects, and more. This was a crucial first step, but a list of services is not enough. A senior engineer must be able to justify why those services exist and precisely where the boundaries between them lie.

Today, we will answer that "why." We'll dive into the principles of Domain-Driven Design (DDD) to formalize our service boundaries. This process is essential for creating a system that is not only scalable and performant but also maintainable and logically aligned with the business problems it solves. By the end of this lesson, you will have a robust mental model for decomposing a complex application into coherent, independent services.

The Problem of a Single, Unified Model

In any sufficiently complex business, the same word can mean different things to different people. For example, in our Construction Management SaaS, the term "user" might mean one thing to the Super Admin managing the platform (an account tied to a subscription), and something else entirely to a Company Admin managing a construction project (a team member with specific permissions on a task).

If we try to create a single, all-encompassing User model to satisfy everyone, it quickly becomes bloated and confusing. This is a classic path toward a "big ball of mud" architecture. DDD provides a structured way to avoid this trap by embracing that different parts of the system require different models.

To get a quick visual intuition for this idea, start by watching this short, animated explainer on Bounded Contexts and Subdomains.

DDD Bounded Contexts & Subdomains

This video from the Drawing Boxes channel provides a clear, concise introduction to the core concepts of Bounded Contexts and why they are necessary.

Watch the full video, paying close attention to the examples of ambiguous language (like "ticket" and "platform") and how defining contexts resolves this ambiguity. Also, note the initial mention of different subdomain types: Core, Supporting, and Generic.


Introducing Domain-Driven Design (DDD)

As the video suggests, DDD is a software design approach that centers on modeling the software to match a business domain. Instead of organizing our code by technical layers (e.g., a "database layer," a "logic layer"), we organize it around business capabilities. Your experience designing RESTful APIs with Django already gives you a foundation here, as good API design often reflects the underlying business entities. DDD provides a more formal framework for this thinking.

The following article from Microsoft's Azure Architecture Center is an excellent, in-depth guide to using domain analysis to model microservices. We'll use it as our primary reference for this lesson.

Using domain analysis to model microservices

This article will walk you through the strategic process of DDD, from understanding the business domain to identifying the bounded contexts that will become our microservices.

First, read the introduction to understand the core problem that DDD solves. Then, read the section that follows, from "Domain-driven design (DDD)" up to the start of the "Analyze the domain" section. Focus on the definitions of Strategic DDD, Ubiquitous Language, and the overall four-step process outlined.


Step 1: Analyze the Domain and Identify Subdomains

The first step in DDD is to understand and map out the business domain. We need to identify the different functional areas, or subdomains, of our Construction Management System. Once identified, we can classify them into three types, which is a crucial step for making strategic architectural decisions.

The Azure article explains this classification well.

Using domain analysis to model microservices

This part of the article explains how to analyze the business functions and classify them into subdomains, using the drone delivery service as a running example.

Read the sections Analyze the domain and Example: Drone delivery application. As you read, pay close attention to the definitions and examples for Core, Supporting, and Generic subdomains.

Now, let's apply this classification to our own project.

  • Core Subdomains: These represent the unique value proposition of our business. This is where we innovate and create a competitive advantage.

    • Project & Task Management: The entire lifecycle of creating projects, defining tasks, setting milestones, tracking progress, and assigning work. This is the heart of our application.
  • Supporting Subdomains: These are necessary for the core business to function, but they don't provide a competitive advantage on their own. We'll likely need to build them ourselves, but they serve the core.

    • Team & Role Management (RBAC): Managing users within a company, creating custom roles, and assigning permissions.
    • Activity Logging / Audit Trails: Recording who did what and when.
    • Notifications: Alerting users to relevant events (e.g., a new task assignment).
    • Document Management: Handling metadata for project files (plans, contracts, photos).
  • Generic Subdomains: These are solved problems. They are critical, but we should not reinvent the wheel. The best strategy is to buy an off-the-shelf solution, use a third-party service, or implement a well-known standard pattern.

    • Authentication: Verifying user identity. This is a standard problem with standard solutions (JWTs, OAuth).
    • Subscription & Billing: Managing company subscription plans and payments. This is almost always handled by integrating a service like Stripe or Braintree.
    • Tenant Onboarding: The process of signing up a new company. While it orchestrates our specific logic, the pattern itself is generic to SaaS.

This classification is incredibly powerful. It tells us where to focus our most creative energy (Core), what we need to build efficiently (Supporting), and what we should leverage from others (Generic).


Step 2: Define Bounded Contexts

With our subdomains identified, we can now define our Bounded Contexts. A Bounded Context is a clear boundary within which a specific domain model—with its own ubiquitous language—is consistent and correct.

This diagram illustrates how a system can be divided into distinct domains (e.g., Product, Developer, Project), each forming a Bounded Context with its own specific models and services.

This diagram illustrates how a system can be divided into distinct domains (e.g., Product, Developer, Project), each forming a Bounded Context with its own specific models and services.

Let's return to the Azure article to formalize this concept.

Using domain analysis to model microservices

This section explains what a Bounded Context is and how to use a "context map" to define the relationships between different contexts.

Read the section Define bounded contexts. Focus on understanding why a single entity (like a drone) can have different models in different contexts.

A Bounded Context often aligns with a subdomain, but not always. This is where the nuance of microservice design comes in. For a senior engineer's perspective on this, the following talk by Vladik Khononov is invaluable. He provides practical heuristics for deciding how and when to break down a system.

Bounded Contexts, Microservices, and Everything In Between - Vladik Khononov - KanDDDinsky 2018

In this talk from KanDDDinsky 2018, Vladik Khononov shares hard-won lessons on decomposing systems into microservices, moving from bounded contexts to subdomains and entities.

This is a rich talk, but let's focus on the core heuristics. First, watch this section to see a real-world example of how conflicting models for the term "lead" forced the creation of two bounded contexts (Marketing and Sales). Next, understand the key insight that a bounded context defines the boundary of the biggest valid monolith. This is crucial—it means a bounded context is your safest, largest unit of decomposition. Finally, watch the section on design heuristics. This is the most important part. Pay close attention to his advice on decomposing based on the subdomain type (Generic, Core, Supporting).


Step 3: Mapping Bounded Contexts to Microservices

Let's synthesize everything we've learned and apply it to our architecture. We'll use the subdomain analysis and Khononov's heuristics to refine the service list from our last lesson.

Heuristic Recap:

  1. Always decompose to Bounded Contexts. This is the baseline to ensure model consistency.
  2. Generic subdomains: Ideal candidates for separate services, often wrapping a third-party tool.
  3. Supporting subdomains: Can be safely decomposed into smaller services because their logic is simpler and more stable.
  4. Core subdomains: Be cautious. Avoid breaking them down into tiny services too early. Keep the core logic together until the domain is stable and well-understood.

Here is our refined service architecture:

Context 1: Identity, Tenancy & Access (The Control Plane)

This context is concerned with managing who can access the platform and what features they are entitled to. It's a collection of supporting and generic subdomains.

  • Subdomains: Tenant Onboarding (Generic), Subscription Management (Generic), Authentication (Generic), User/Team Management (Supporting).
  • Service Boundaries:
    • Auth Service: Handles the generic problem of authentication. Its responsibilities are narrow: validating credentials and issuing JWTs. It owns the user identity in its most basic form (email, hashed password).
    • Tenant Service: This service manages the "company" or "tenant." It handles company registration, subscription plans, user invitations, and role definitions (RBAC). It's the primary "control plane" service for managing tenant lifecycle. The User model here is richer, including their role and membership within a tenant.
  • Rationale: We are separating the truly generic task of authentication from the business-specific logic of tenant and subscription management. This follows the heuristic of extracting generic and supporting subdomains into their own services.

Context 2: Project Collaboration (The Application Plane)

This context is our Core domain. It's where the primary value of our product is delivered.

  • Subdomains: Project & Task Management (Core), Document Management (Supporting), Activity Logging (Supporting).
  • Service Boundaries:
    • Project Service: This service will house our core business logic. It manages projects, tasks, milestones, and their relationships. Following the "don't decompose the core too early" heuristic, we will keep this logic cohesive within a single service to allow for rapid iteration and refactoring as we build out the main features. The User model in this context is that of a "participant"—an assignee, a commenter, a project member.
    • File Service: This service handles the supporting subdomain of document management. Its responsibility is not to store files, but to manage their metadata and orchestrate secure interactions with a dedicated cloud storage service like AWS S3 (e.g., by generating pre-signed upload URLs). Separating this allows it to be scaled independently and keeps the Project Service focused on core logic, not on cloud storage integration details.
  • Rationale: We are protecting our core domain by keeping it in one place while offloading distinct, supporting responsibilities to specialized services.

Context 3: Notifications

This is a classic supporting domain that interacts with many other parts of the system.

  • Subdomain: Notifications (Supporting).
  • Service Boundary:
    • Notification Service: A dedicated service for creating, managing, and sending notifications. It will listen for events from other services (like "task assigned" from the Project Service) and dispatch alerts via various channels (in-app, email, etc.).
  • Rationale: The Render article (LINK) and common microservice patterns identify notifications as a prime candidate for extraction. It has independent scaling needs and a clear, single responsibility.

Here is a diagram showing how a request might flow through one of these services. Notice how the tenant identity, extracted from the JWT, is passed through all internal components to ensure strict data isolation. This pattern will be implemented inside each service within our Application Plane.

This diagram illustrates the internal architecture of a single multi-tenant service, like our Project Service. The JWT provides the tenant context, which is used to scope all operations, from logging to database access.

This diagram illustrates the internal architecture of a single multi-tenant service, like our Project Service. The JWT provides the tenant context, which is used to scope all operations, from logging to database access.


Conclusion

Today we moved from a rough sketch of our architecture to a well-reasoned design based on the principles of Domain-Driven Design. By defining clear service boundaries, we've set ourselves up for a more maintainable, scalable, and business-aligned system.

Key Takeaways:

  • DDD aligns software with business domains. It helps us manage complexity by creating clear models for different parts of the business.
  • Bounded Contexts are explicit boundaries where a specific model and language are consistent. They are the foundation for our service decomposition.
  • Classifying subdomains (Core, Supporting, Generic) provides a strategic guide for deciding where to focus development effort and which components are candidates for extraction into separate services.
  • Our refined service list—Auth, Tenant, Project, File, and Notification services—reflects these principles, creating a logical and robust starting point for development.

Preview of the Next Lesson:
With our high-level architecture and service boundaries now defined, we will begin to transition into low-level design. In the next lesson, we will map out one of the most critical user flows: new company registration and initial tenant provisioning. We will design the entire sequence, from a user clicking "Sign Up" to their company being provisioned in the system and them logging in for the first time. This will force us to think about the concrete interactions between our newly defined services.