A pragmatic approach to build billion-dollar apps with a small team
Introduction
How can you quickly fail with a Java project? Just pick every new technology from the latest conference without having a clear plan. But how can you build a big, reliable project quickly—even with a small team? The secret is choosing the right architecture and easy-to-use tools like Jmix, which helps you build enterprise applications faster and easier.
This article explains a useful software architecture called Self-Contained Systems (SCS).
With SCS, you split your big application into smaller parts, called domains. Each domain is like a small, independent application. The key idea is to make these small applications talk to each other very little. This is akin to microservices, but simpler and easier to manage.
We learned about SCS by working on large, complicated projects. In this article, we share what we learned and show how SCS can help solve many real-life problems. We will also talk about how you can build billion-dollar projects using Jmix and how jmix makes it easier to build systems based on SCS quickly.
The Evolution of Architectures
Software architecture has evolved through several phases, each addressing specific needs:
1. Early Web Architectures (e.g., MVC)
Initially, their focus was on delivering static content and simple request-response cycles. As projects grew in complexity, architectures like MVC helped scale codebases.
2. N-Layer and SOA
As systems became more interconnected, layered architectures and Service-Oriented Architecture (SOA) emerged to facilitate integration, data exchange, and code reuse. This approach improved internal communication, yet scalability still mainly boiled down to adding layers of code complexity rather than addressing team size or resource constraints.
3. Microservices Era:
Microservices changed the game by decomposing systems into more minor, independently deployable services. Different teams could own services and scale resources (CPU, memory, storage) for specific system parts. Microservices looked like a promising solution in terms of agility, continuous deployment, and architectural freedom. However, they also introduced substantial complexity, requiring intricate DevOps tooling, as well as extensive documentation and coordination.
4. Disenchantment and Reassessment:
Over time it became clear that the full scale-out capabilities and complexity of microservices was not for everyone. For many enterprise and e-commerce projects — especially those not operating at the “Big Tech” scale—the overhead brought forward by microservices became burdensome. Teams began looking for a middle ground solution, which preserved some modularity and fault tolerance without incurring the full complexity costs of microservices.
Self-Contained Systems
An Overview of Self-Contained Systems
Self-contained systems (SCS) are small, independent applications which, taken together, form one bigger software product. Each SCS application focuses on one specific part (domain) of the whole system. They work separately and don't depend much on each other.
You can think of SCS as something between microservices and a big monolith. Microservices are very small and can be hard to manage. A monolith is easy to build but hard to change. SCS gives you the best parts of both approaches—simple to manage, easy to change, and more stable. If one part fails, the other parts still work.
Here, you would typically see diagrams showing clear examples of monoliths, SOA, microservices, and SCS. These diagrams help you understand each architecture. Usually, SCS diagrams show multiple independent domain monolithic applications communicating through simple messages.
The Philosophy of SCS

The SCS community (notably Innoq) outlines several guiding principles:
- Domain-Centric Decomposition: Break down the system by domain, with each domain as an autonomous subsystem.
- Domain Ownership: Subsystems should not expose or import internal domain objects unnecessarily.
- Minimal Overhead: Each subsystem resembles a microservice in architecture but can remain a monolith internally.
- Service Orientation: Internally, a subsystem can contain multiple microservices, but externally it acts as a single unified system.
- Dedicated Databases: Each subsystem manages its own data storage.
- Technological Freedom: Teams choose the best technology stack without strict constraints from other subsystems.
- Asynchronous Communication: Domains usually use REST APIs or simple asynchronous message brokers, depending on the situation (as an exception, they can also be synchronous).
- Communication Independence: If one subsystem is down, others should continue operating or gracefully handle the unavailability. By the way, cross-system communications do not have to break any business processes just because of the unavailability of dependent subsystems.
- Team Alignment: Each subsystem is owned by a single team (though one team can own multiple subsystems).
- Minimal Coupling: Dependencies between subsystems should remain low. Using clear UI integration strategies can help reduce coupling further.
- Reusability: Common functionality, UI components, or DTOs can be managed in shared libraries/toolkits.
- Separate UIs with Consistent Style: Each subsystem has its own user interface. However, all UIs should follow the same style using a shared UI toolkit or clear style guidelines.
- Unified Look and Feel: Users should feel like they are using one application despite the presence of multiple subsystems. All subsystems should look and behave similarly, using a shared UI kit.
- Seamless Integration: From a user’s perspective, the subsystems present a unified experience (e.g., via hyperlinks, iframes, or a root layout).
- Bi-Directional Links: Subsystems should allow cross-navigation via hyperlinks where it makes sense.
These principles are guidelines, not strict rules. Following them closely helps avoid future problems. Also, principles 1–11 might look similar to what we had with microservices. However, the last three (12–15) are SCS-specific. They ensure that each domain’s user interface looks good and feels easy to use, even though the domains are separate. They also help domains connect smoothly for the user.
Advantages and Disadvantages of SCS
Advantages
- Fault Tolerance: If one subsystem fails, the others can still continue working normally.
- Independence for Teams: Each subsystem can be developed and maintained separately.
- Clear Domain Boundaries: Encourages separating functionality clearly into independent modules or domains.
- Keeps Monolithic Simplicity: Allows fast development and easy changes, similar to building one big application.
- Independent Deployments: Subsystems can be updated, scaled, and released independently.
- Good for Enterprise: Usually simpler and cheaper than microservices, especially for large business applications.
Disadvantages
- Complex UI Integration: Building multiple subsystems that look and feel like one application can be tricky.
- Communication Effort: Needs careful planning to connect subsystems, unlike a simple monolithic application.
- Deployment Complexity: Deploying SCS is simpler than microservices, but still more complex than a single monolith.
- Extra Documentation: You must clearly document how each subsystem works and interacts with others.
- Shared UI Components: All teams must use common UI tools or guidelines, which takes extra effort.
- UI Performance Challenges: Improving user interface speed and responsiveness is often harder across multiple subsystems compared to a single large application.
Note: For many business or e-commerce projects, these disadvantages are less important than having reliable and clear domain-specific features.
Why Jmix is a Good Fit for SCS?
Self-Contained Systems (SCS) is a practical and proven architectural pattern for building modular software. But like any other architectural solution, its success depends on the right tools. In practice, we’ve found that Jmix aligns well with the core ideas of SCS, making it easier to implement in real-world scenarios.
Here's exactly how Jmix supports SCS projects:
- Rapid Domain Development: Quickly builds separate, independent domains with ready-to-use tools and built-in patterns.
- Built-in Security and User Management: Jmix handles authentication, permissions and roles out-of-the-box, significantly reducing development complexity.
- Simplified Domain Integration: Jmix’s built-in REST APIs simplify the process of connecting multiple domains smoothly.
- Consistent and Modern UI: With the additional help of Vaadin,, Jmix provides easy-to-build, modern user interfaces without additional frontend overhead, allowing each domain to maintain UI consistency.
- Robust Workflow Automation: Built-in support for business process automation (using BPM tools like Flowable) allows domains to interact reliably and asynchronously.
- Flexible Technology Base (Spring Boot): Built upon Spring Boot, Jmix offers extensive flexibility and compatibility with existing Java and Kotlin ecosystems.
- Scalable and Maintainable Monoliths: Jmix helps build clear, maintainable Java or Kotlin monoliths without unnecessary complexity, aligning well with SCS principles of simplicity and low coupling.
Our focus on SCS comes directly from observing how naturally Jmix supports and simplifies this architectural approach. Combining SCS with Jmix results in easier, faster, and more reliable software development for enterprise-level projects.
For instance, consider a food delivery application. With Jmix, we rapidly set up distinct domains like orders, restaurants, and couriers. Each domain has its own database, UI, and business logic, while Jmix ensures straightforward integration between them. Complex tasks, such as assigning couriers or handling restaurant orders, become significantly simpler.
Later in this article, we'll show you precisely how to build this food delivery application using Jmix and further explore the practical benefits of this combination.
Before diving into how Jmix fits into SCS, let’s first explore a straightforward case where the SCS approach clearly solves common architectural challenges. This will help show why SCS makes sense—even before we bring in any specific technology.
An Ideal Scenario for SCS: AI Chat Platform
Imagine a platform where you can chat with an AI assistant, generate images, analyze documents, and even create code — all in one place. For example, think about something like OpenAI’s ChatGPT, but with extra features for images, files, and code. To the user, this looks like one big product. But inside, it is made up of several smaller, independent systems.

Why Not Monolith or Microservices?
- If you build everything as one large monolith, it quickly becomes hard to manage and scale.
- If you go with full microservices, you might spend too much time and money on it.
Self-Contained Systems (SCS) offer a third way. Each big feature (chat, image generation, document analysis, code generation) becomes a standalone system — called a domain of subsystem. Each domain has its own UI, logic, and database. Still, from the user's point of view, it all works as a single product.
Applying SCS to an AI chat platform
Domains as Subsystems
Let’s break this platform into clear, independent domains:
- Chat System: Handles conversations with the user. It can use internal services like language models or embeddings, but always presents one simple interface.
- Image Generation System: Creates images from user’s prompts.
- Document Analysis System: Lets users upload and analyze documents.
- Code Generation System: Helps users by creating code based on their questions or prompts. It can use AI models or special algorithms to generate and explain code.
- Integrations / Partner Services: Connects to third-party services or external APIs.
Each subsystem:
- Has its own user interface (UI)
- Manages its own database
- Runs its own backend application
Sometimes, a subsystem may also include smaller internal services for extra processing or calculations. Subsystems can communicate through APIs or asynchronous messages. For example, if a user asks for image generation inside the Chat System, it sends a request to the Image Generation System and either receives a result or, if that system is unavailable, simply informs the user that the feature is not ready right now. This kind of seamless interaction between subsystems makes the whole platform feel like a single, unified product—even though it is built from separate parts.
This design lets every domain develop and improve independently, but together they create one smooth product for the user.
Unifying the UX
Even though each domain can run on its own, users want the whole platform to feel like one product. SCS gives you several simple ways to create a unified and smooth experience for everyone.
Root Layout and Navigation
Usually, there is a main application — sometimes called a “root layout”.
This part provides global navigation and look and feel. The main app can show other domains in iframes or by linking to them. All domains use the same UI kit to keep the style familiar for the user.
Example layout:
- Top navigation with links to Chat, Image Generation, Document Analysis, etc.
- Main content area displays the chosen subsystem’s UI (for example, via an iframe).
Extra panels can show history or context (like recent chats or images).
Hyperlinks and Context Passing
Domains can link to each other using special URLs. For example, after generating an image, the Image System can give a link to the Chat System with the image’s ID. When the user clicks on it, the chat opens and shows the image inside the conversation. This way, domains stay separate, but users get a smooth workflow.
Asynchronous Integration
Sometimes, domains send requests and wait for responses in the background.
For example, if the user asks for an image in the Chat System, the chat sends a request to the Image Generation System. When the image is ready, it sends a link back. If the image system is offline, the chat just tells the user “this feature is not available right now.” This keeps the experience smooth and reliable, even if some parts are down.
Cross-system interactions: A practical example
Let’s say a user currently interacts with the Chat (GPT) System and requests image generation. In a typical synchronous or hybrid approach the system world:
Check Availability: The GPT System pings the DALL·E (Image Generation) System to confirm that it is online.
- Delegate Request: If available, the GPT System forwards the user’s prompt (e.g., “Generate a robot dancing on Mars”) to the DALL·E subsystem.
- Await Response: The GPT System waits for the outcome (either synchronously or asynchronously).
- Provide Feedback:
a. Success: Returns the generated image link or a small preview to the user.
b. Failure: If DALL·E is down, the GPT System immediately replies, “Image Generation is unavailable now.”
This setup ensures that each subsystem is autonomous. A DALL·E failure doesn’t break chat functionality — it only affects the image request.
Autonomy, Resilience, and Flexibility
Why this is useful:
- Each domain can work, scale, and update by itself.
- If one domain fails, others keep running.
- New domains (like Audio Generation) can be added easily later—just plug them in.
Minimal Inter-system Coupling:
- Each domain hides its inner logic.
- Data is never shared directly — domains use stable APIs to talk.
- Teams can update or improve one domain without breaking others.
User-Centric by DDD Approach:
- Each domain matches a user’s real task (chat, images, docs).
- Each domain knows its job well, so it is easy to improve over time.
Summary: Why SCS Works Here
In an SCS-based AI platform, each domain is like a “mini-app.” It manages its UI, business logic, and data independently. The root layout brings them together with iframes, links, and shared UI. The whole platform looks and works as one product but is easy to scale and maintain.
What this gives?
Fault tolerance — one domain fails, the rest keeps working.
- Scalability: scale only the most busy domains.
- Flexibility: add or change features with little effort.
- User focus: architecture matches how users see the product.
The Quasi-SCS example
Yet another food delivery demo
Food delivery is a simple, real-world scenario everyone understands. It has independent subsystems (orders, restaurants, couriers), asynchronous flows, and easy roles — the perfect sandbox for demonstrating SCS. In fact, it’s even clearer than the AI chat example from earlier.
Why Jmix?
Jmix lets you build complex business logic, UI, and user roles fast. Out-of-the-box BPM (Flowable) handles business processes, and security is easy with Keycloak. We’re showing practical, step-by-step SCS architecture — not just theory, but real code.
End-to-End Business Flow
Let’s see how a real food delivery process works before we jump into code.
Domain Breakdown: Who Owns What?
Let’s start by mapping out the key domains (subsystems) in the food delivery scenario:
- Order System:
Handles all user-facing interactions: selecting food, placing orders, tracking order status. Orchestrates the business process and acts as the “brain” of the flow. - Restaurant System:
Manages restaurant data, menus, food items, and processes requests to prepare orders. Restaurant admins confirm the order preparation. - Courier System:
Handles couriers, assigns delivery tasks, and updates order delivery status.
Note: In a full enterprise implementation, you’d likely have additional domains for payments, notifications, reviews, etc. For this demo, we keep it to three for clarity.
Step-by-step process
Here’s what happens with business process, step by step, from the user’s perspective:
-
User places an order:
- Views the restaurant list (from Restaurant System).
- Builds a cart, places an order.
-
Order System launches a business process:
- A new BPMN process instance tracks the order.
-
Restaurant confirmation:
- The process sends a request: “Can you cook this order?”
- Waits for the restaurant admin to accept.
-
Cooking confirmed:
- Restaurant admin confirms in their UI (also Restaurant System).
- Process resumes.
-
Courier assignment:
- Process requests a courier from Courier System.
- Waits for a courier to accept.
-
Delivery:
- Courier marks as delivered.
- Process ends, status is updated.
All long-running "waits" are handled asynchronously, thanks to Jmix BPM Engine.
Delivery business flow
Before we dive into the code, let’s look at a high-level diagram that illustrates the business process for food delivery. This will help you get a sense of the overall flow before we break it down step by step.
You might notice that I often use “monolith-style” diagrams to illustrate the delivery flow—even though our project is based on SCS principles. There’s a simple reason:
I built two versions of the application — one as a classic monolith and another using SCS. The monolithic version is much easier to visualize and explain, especially for readers who are new to these patterns. Our open-source example (see GitHub) uses the SCS approach, but is intentionally simplified — only three domains are implemented, and the BPMN isn’t 100% production-ready. For this article, I’ll focus on the simplified app, not a full-scale enterprise implementation.
If you really want to dive deep into a canonical SCS diagram for food delivery, feel free to check the full architecture in the repository.
But my advice is simple: Don’t stick too much to diagrams and business requirements.
Focus instead on the core ideas and follow the step-by-step walkthrough below.

By the way, BPMN diagrams make it much easier to understand how the process flows. So, if the system diagrams seem overwhelming, just skip ahead to the BPMN examples — they’ll clarify the process much more clearly.
Below BPMN diagrams make the process much clearer, so feel free to skip ahead to those examples if the system diagrams seem too complex.
BPMN in Action
Orchestration is managed in a BPMN process:
- Service tasks (Gear wheel): Automated steps (like HTTP calls to Restaurant System).
- User tasks (User icon): Wait for admins or couriers.
- Timers / errors (Clock): Retry, timeout, or alternate flows.
We’ll see exact BPMN XML fragments in the next sections. Now, let’s look at how the Restaurant System is built in Jmix — with real code, REST endpoints, and UI screens.
Building all together
Let’s start from the Restaurant domain. Why? The Restaurant System is a core domain in our food delivery SCS example. This is where restaurant admins manage menus, food items, and confirm cooking requests. Let's see how we implement this subsystem in Jmix — step by step, with real code and business logic.
1. Defining the Main Entities
First, we define the main data structures.
For restaurants, we need entities like:
- Restaurant — the main entity for a restaurant.
- RestaurantMenu — a menu belonging to a restaurant.
- RestaurantMenuItem — individual dishes, belongs to the menu of the restaurant.
Example Jmix entity for Restaurant:
@JmixEntity
@Table(name = "RESTAURANT")
@Entity
public class Restaurant {
@JmixGeneratedValue
@Id
private UUID id;
private String name;
private String description;
// Optional: image/icon as attachment
}
Other entities are similar, each with references back to Restaurant.
2. Creating UI Screens with Jmix FlowUI
With Jmix, you don’t need to write boilerplate UI code.
You can generate or customize CRUD screens for all entities:
- Restaurant List Screen: Shows all restaurants.
- Restaurant Detail Screen: Allows admins to edit adn to add menus and dishes.
- Menu/Item Screens(Pages): Manage menu composition.
The screens are defined declaratively and can be customized as needed.
Note: In Jmix 2 any web pages are named “Views”, in the older Jmix versions the name was “Screens”.
3. Exposing REST API for Inter-Domain Communication
To allow the Order System to fetch restaurants/menus, we expose a simple REST API:
@Secured(FullAccessRole.CODE)
@RestController
@RequestMapping("/api/v1")
public class RestaurantController {
@Autowired
private RestaurantRepository restaurantRepository;
@GetMapping("/restaurants")
public List<RestaurantDTO> listRestaurants() {
return restaurantRepository.findAll().stream()
.map(restaurant -> {
var dto = new RestaurantDTO();
dto.setId(restaurant.getId());
dto.setName(restaurant.getName());
dto.setDescription(restaurant.getDescription());
// Set icon if available
return dto;
})
.toList();
}
@GetMapping("/restaurants/{id}")
public RestaurantDTO getRestaurant(@PathVariable UUID id) {
// fetch and map Restaurant to DTO
}
@GetMapping("/restaurants/{restaurantId}/menus")
public List<RestaurantMenuDTO> listMenus(@PathVariable UUID restaurantId) {
// fetch and map menus for given restaurant
}
}
4. Confirming Cooking Requests
Orders are passed from the Order System via a REST call, which the restaurant admin needs to confirm.
Receiving a cook request:
@PostMapping("/restaurants/{restaurantId}/cook")
public String getRestaurantCookRequest(@PathVariable UUID restaurantId, @RequestBody OrderDTO orderDTO) {
// Save a new cook order for admin review
cookOrderService.submitNewCookOrderFromDTO(orderDTO);
return "Accepted";
}
Admins see pending cook requests in their Jmix UI and can accept them.
4a. The Service Layer: Business Logic in Jmix
Let’s look at the CookOrderService, which handles business logic for creating new cooking requests.
Here’s a simplified version of the relevant method:
@Service
public class CookOrderService {
@Autowired
private RestaurantRepository restaurantRepository;
@Autowired
private DataManager dataManager;
public void submitNewCookOrderFromDTO(OrderDTO orderDTO) {
CookOrderRequest cookOrderRequest = dataManager.create(CookOrderRequest.class);
cookOrderRequest.setOrderId(orderDTO.getOriginOrderId());
cookOrderRequest.setIsDone(false);
cookOrderRequest.setRestaurant(restaurantRepository.getById(orderDTO.getRestaurantId()));
cookOrderRequest.setCookingItems(createCookingListFromDTO(cookOrderRequest, orderDTO));
dataManager.save(cookOrderRequest);
}
// Helper for creating cooking item list (not shown for brevity)
}
What happens:
- This method receives an order, creates a CookOrderRequest entity, links it to the restaurant, and saves everything to the database.
- Jmix’s DataManager handles persistence and transactions, reducing boilerplate.
This is the kind of domain service you’ll find in each subsystem. It keeps REST controllers thin and business rules clear.
5. How it fits into the BPMN process
The Restaurant System is involved at these BPMN steps:
- Receives an automated service task ("request restaurant to cook").
- Waits for a user task (admin confirmation).
- Sends callback to the Order System to resume the process.
(BPMN XML fragment — as promised!)
<serviceTask id="Activity_1jhcclq" name="Request Restaurant to Cook"
flowable:async="true"
flowable:expression="${requestRestaurantCookStep.execute(execution)}"
jmix:taskType="springBean"
jmix:beanName="requestRestaurantCookStep">
<!-- ... -->
</serviceTask>
<userTask id="WAIT_RESTAURANT_CALLBACK_TASK" name="Wait for Restaurant Confirmation">
<!-- ... -->
</userTask>
Key Takeaways
- Clear domain boundary: Restaurant System manages only its own data and UI.
- Integration is explicit: All cross-domain operations are via REST API, not direct DB calls.
- Jmix FlowUI + REST = quick setup: UI and APIs are generated or customized fast.
- Human in the loop: Restaurant admin interacts via UI, tying into the BPMN flow.
How it fits into the BPMN Process
The BPMN engine (Flowable, integrated in Jmix) orchestrates the entire food delivery process as a long-running workflow. Each external interaction (like waiting for a restaurant admin to confirm cooking) is represented as a User Task in BPMN.
How does the callback work?
- When the admin in the Restaurant System confirms a cooking request, the system sends an HTTP callback to the Order System.
- The Order System finds the paused BPMN process instance for that order and moves it to the next stage.
Example: Continuing the Order Process after Restaurant Confirmation
public void continueOrderRestaurantStep(@PathVariable String orderId, @PathVariable String restaurantId) {
orderProcessManager.continueProcessByRestaurantStep(orderId, restaurantId);
}
Service method to continue the process:
public void continueProcessByRestaurantStep(String orderId, String restaurantId) {
OrderEntity order = orderRepository.getById(UUID.fromString(orderId));
// Check if the callback is from the correct restaurant
if (!order.getRestaurantId().toString().equals(restaurantId)) {
throw new RuntimeException("Illegal restaurant callback");
}
// Find the waiting user task in the BPMN process and complete it
continueUserTaskInProcess(orderId, "WAIT_RESTAURANT_CALLBACK_TASK");
}
private void continueUserTaskInProcess(String orderId, String taskDefinitionId) {
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceBusinessKey(orderId)
.singleResult();
Task userTask = taskService.createTaskQuery()
.processInstanceId(processInstance.getId())
.active()
.taskDefinitionKey(taskDefinitionId)
.singleResult();
taskService.complete(userTask.getId());
}
What does this achieve?
- The BPMN process resumed exactly at the point where it was waiting for confirmation.
- The workflow then moves on to the next automated or manual step (e.g., requesting a courier).
BPMN XML snippet for this step:
<userTask id="WAIT_RESTAURANT_CALLBACK_TASK" name="Wait for Restaurant Confirmation">
<extensionElements>
<jmix:formData type="no-form" />
</extensionElements>
<incoming>Flow_0b7vho3 (randomly generated id’s of next task)</incoming>
<outgoing>Flow_1wegar9 (randomly generated id’s of outgoing task)</outgoing>
</userTask>
This clear separation between service logic, UI, and the business process state makes your application resilient and easy to extend. If something goes wrong (restaurant doesn’t confirm in time, etc.), you can handle it in the BPMN diagram (e.g., add timeouts, errors or retries).
Courier Assignment and Delivery
After the restaurant confirms the order, the process requests a courier and tracks the delivery. Everything is organized just like before: via dedicated REST requests and user/service tasks in BPMN.
Requesting a Courier (Service Layer)
When the BPMN process reaches the “Find Courier” step, a service task sends an HTTP request to the Courier System to publish a new delivery task:
@Service
public class RequestCourierDeliveryStep extends AbstractTransactionalStep {
private final CourierClient courierClient;
private final OrderService orderService;
@Override
protected void doTransactionalStep(DelegateExecution execution, OrderEntity order, SaveContext saveContext) {
String username = getVariable(execution, PROCESS_USER_KEY);
String subjectToken = exchangeOidcTokenForUser(username);
// Send request to Courier System
String result = systemAuthenticator.withUser(username,
() -> courierClient.publishCourierDeliveryRequest(order.getId(), orderService.convert(order), subjectToken));
order.setStatus(DraftOrderStatus.WAIT_FOR_COURIER);
saveContext.saving(order);
}
}
CourierClient:
public String publishCourierDeliveryRequest(Long orderId, OrderDTO orderDTO, String subjectToken) {
String url = MessageFormat.format("{0}/api/v1/couriers/delivery/{1,number,#}", courierUrl, orderId);
return getApi(url, HttpMethod.POST, new ParameterizedTypeReference<String>() {}, orderDTO, subjectToken);
}
Courier System: Accepting a Delivery
A courier logs into the corresponding system, sees new orders, selects one, and the system sends a callback HTTP request back to the Order System:
public void continueOrderCourierStep(@PathVariable String orderId, @PathVariable String courierId) {
orderProcessManager.continueProcessByCourierStep(orderId, courierId);
}
Process continuation on courier callback:
OrderEntity order = orderRepository.getById(UUID.fromString(orderId));
if (!order.getCourierId().toString().equals(courierId)) {
throw new RuntimeException("Illegal courier callback");
}
continueUserTaskInProcess(orderId, "COURIER_WAIT_FOUND_TASK");
}
In UI from Courier perspective this step described below:
BPMN XML Snippet: Waiting for Courier
<extensionElements>
<jmix:formData type="no-form" />
</extensionElements>
<incoming>Flow_1f3t4wk</incoming>
<outgoing>Flow_1f0p9t6</outgoing>
</userTask>
Delivery Completion
Once the courier delivers the order, they call another endpoint in the Order System:
public void continueOrderDeliveredStep(@PathVariable String orderId) {
orderProcessManager.continueProcessByDeliveredStep(orderId);
}
// Complete the user task and advance the BPMN process
continueUserTaskInProcess(orderId, "COURIER_DELIVERED_TASK");
}
Which we call from UI in Courier system:
BPMN XML Snippet: Delivery Wait
<extensionElements>
<jmix:formData type="no-form" />
</extensionElements>
<incoming>Flow_1c7s5el</incoming>
<outgoing>Flow_123t2l0</outgoing>
</userTask>
Now, from the client’s perspective the delivery request with the DONE status looks as follows:
Why is this powerful?
- Each domain interacts through clear API boundaries and the BPMN-controlled process state.
- All “waiting” and “human” steps are managed in BPMN — which provides convenient support for timeouts, cancellation, escalation, or retries.
- No tight coupling or shared state between systems: each subsystem can go offline, be replaced, or scaled, and the process will just “pause” at the right place.
Note: For demo purposes, I put BPM Engine into the Order System, but for more stability, it is better to have Engine as a Standalone server (or as a System).
Error Handling, Timeouts, and Final Delivery: BPMN and Jmix Patterns
In real business processes, things go wrong: a restaurant might not confirm, a courier might disappear, or a network glitch could interrupt the flow.
SCS and BPMN allow you to design for resilience—handling these failures explicitly, not sweeping them under the rug.
Handling Errors and Timeouts (BPMN)
BPMN lets you model timeouts and errors directly in the process definition, so your code stays simple, and the business logic stays visible.
Example: Timer on Restaurant Confirmation
Suppose, the restaurant admin doesn’t confirm the order in time. In the BPMN, we add a timer boundary event to the “Request Restaurant Confirmation” step:
<outgoing>Flow_1nr0tno</outgoing>
<timerEventDefinition>
<timeDuration>PT1M</timeDuration>
</timerEventDefinition>
</boundaryEvent>
<sequenceFlow id="Flow_1nr0tno" sourceRef="TIMER_ON_RESTAURANT" targetRef="Activity_05ssv9d" />
If the timer fires (e.g., after 1 minute or in terms of BPMN - PT1M), the process moves to an “Order Cancelled” step, and the user gets notified:
<incoming>Flow_1nr0tno</incoming>
<outgoing>Flow_0hj5ypr</outgoing>
</task>
<incoming>Flow_0hj5ypr</incoming>
</endEvent>
Similar timers can be added for courier assignment and delivery.
Clean Error Propagation
Because the BPMN engine drives the process, your service code can stay focused: throw an error, and the process will handle it.
If any REST request fails (network error, HTTP 500, wrong input), you can catch it and throw a BPMN error. The process will move to the error handler, showing the user a clear message or triggering a retry.
Example in Java:
// ... REST call to Restaurant System
} catch (Exception ex) {
throw new BpmnError("ORDER_CANCEL_ERROR", "Could not confirm order with restaurant");
}
Final User Experience
If you need better understanding of what is happening in user flow, this sub-article in the GitHub Repository is for you.
- The user always sees a clear, up-to-date order status in the Order System UI.
- If something fails (timeout, cancellation), the UI shows an error message or “please try again.”
- All process transitions are visible to admins and users — nothing is hidden in backend logs.
Summary: Why SCS + Jmix + BPMN?
- Separation of Concerns: Each subsystem is fully independent: failure in one of the subsystems does not cascade
- Explicit Processes: BPMN puts the real business flow front-and-center. Business users and developers can read and update it.
- Rapid Iteration: Jmix + Flowable BPM lets you add steps, UI, or logic without rewiring everything.
- Enterprise-Ready: Security (Keycloak), forms/UI, and process orchestration are all handled without tons of custom boilerplate.
Conclusion for the Jmix Delivery Example
By walking through a real-world food delivery scenario with Jmix, you’ve learned:
- How SCS breaks up complex domains and business flows into truly autonomous systems.
- How Jmix helps rapidly build robust UIs, APIs, and back-office screens for each domain.
- How BPMN lets you orchestrate (It is unclear what the author is trying to say here), monitor, and evolve end-to-end processes that span domains.
- How clear API boundaries, process-driven integration, and explicit error handling make your project more resilient and maintainable.
But here’s the twist:
Our implementation isn’t a textbook SCS. We made a few pragmatic adjustments:
- We skipped a root layout shell — every domain has its own UI, and users simply access the needed subsystem directly (or put end-to-end bidirectional links like “Want to become courier? -> Courier system link”)
- Instead of pure asynchronous messaging between domains, we used BPMN-driven orchestration for all long-running or “waiting” steps. This actually gave us a more robust and transparent business process, easier to troubleshoot and extend.
Note: Sometimes, “bending the rules” makes the architecture even stronger for your team and use case.
If you want to explore or extend this demo, check out the full project on GitHub.
And remember: SCS isn’t about buzzwords — it’s about building resilient, understandable systems with the right tools for the job.
What about the “root layout” dream?
Of course, there are scenarios where you need a truly unified user experience across all subsystems — a seamless UI, deep cross-domain navigation, and the feeling that everything is “one big product.” The classic example? Amazon’s ecosystem.
Let’s wrap up with a quick look at how SCS can power even the most integrated, product-like platforms.
Amazing “root layout” case: Amazon’s ecosystem
Amazon’s website is a textbook “root layout” example for Self-Contained Systems, even if it isn’t called that explicitly. To the user, it’s a single unified experience. Behind the scenes, it’s built from autonomous subsystems — each responsible for a major part of the business.
It might not publicly brand itself as employing Self-Contained Systems, but its vast range of services can be conceptualized similarly. Amazon’s platform appears as a single, unified website to the end-user. However, underneath the hood, you can imagine multiple autonomous subsystems:
- Product Catalog and Search: Provides product listings, filters, and recommendations.
- Shopping Cart and Checkout: Manages the cart, payment methods, discounts, and order confirmations.
- Account Management and Settings: Handles user profiles, order histories, and personal data.
- Streaming Services (Prime Video, Music): Each run as a self-contained subsystem with its own UI and logic, yet remains accessible from the main navigation.
From a user’s perspective, navigating from “Shop by Department” to “Prime Video” is seamless—even though these could be entirely different domains and tech stacks. In reality, these services communicate minimally (often via REST APIs or well-defined contracts), but the UI remains consistently branded for a cohesive look and feel. If a subsystem like Prime Video experiences issues, the rest of Amazon is still available—demonstrating fault tolerance and autonomy.
Conclusion
Self-Contained Systems (SCS) offer a practical middle ground between classic monoliths and sprawling microservices. Whether you’re building a modern AI platform, a food delivery service, or a complex e-commerce site, SCS principles help you:
- Divide your application into well-defined, domain-focused subsystems
- Enable team autonomy, independent deployment, and robust fault isolation
- Deliver a seamless, unified experience to your users
There’s no one-size-fits-all in software architecture. But with SCS, you gain flexibility—adopting the principles fully or partially, as your business and the tech context demand. The result: systems that are easier to change, more resilient to failure, and closer to how users and teams actually work.
Adopt SCS where it fits — and enjoy software that scales and evolves without unnecessary pain.