All articles

Implementing Domain-Driven Design in Enterprise Java Applications with Jmix

What Is DDD, and Why Should You Care?

Building software is hard. Building software that scales well, stays maintainable, and performs under real-world pressure is even harder. The industry has produced dozens (if not hundreds) of approaches, patterns, and paradigms to tackle this challenge. This article offers a pragmatic take on one of them: Domain-Driven Design.

The textbook definition — from Eric Evans' Domain-Driven Design (2004) — goes something like this:

Domain-Driven Design (DDD) is a set of principles and patterns aimed at creating optimal object systems. It centres on building software abstractions called domain models, which encode business logic that connects real-world conditions to code.

That’s a mouthful. To put it simply: DDD means building an application as if you were explaining it to the business, not to a programmer.

The approach rests on several core principles. In this article, I’ll walk through the main ones and a few supplementary ones, illustrating each with examples drawn from an ERP system for a small manufacturing company. Along the way, we’ll see where Jmix can be useful.

DDD Principles at a Glance

Here’s a quick overview of what we’ll cover:

  • Ubiquitous Language — a shared vocabulary for everyone
  • Bounded Contexts — autonomous modules with clear responsibilities
  • Aggregates — encapsulated business rules
  • Entities and Value Objects — modelling reality, not database tables
  • Core Domain — isolating the domain model
  • Domain Events — reacting to meaningful state changes

Here’s a closer look at each one.

Ubiquitous Language (Core Principle)

Every participant in the development process — developers, analysts, testers, and the business itself — must use the same terms to describe the application’s processes, data, and rules. When the language drifts between conversations and code, understanding drifts with it.

Anti-pattern — technical jargon obscures business meaning

A production technologist says: “Cutting a sheet of plywood wastes about 20%. For a run of 100 units, we need to order extra material.” The developer creates tables named materials, waste_coefficient, production_requests. Where is “cutting layout” in that schema? What does waste_coefficient actually mean? The business meaning is lost. A new developer joining the team may never recover it.

Better approach — domain terms become classes and methods

class TechnologyMap {  

private Material material; 

 private Decimal wastePercent; // "waste"

private Integer resultQty; // "cutting yield"

public MaterialCalculation calculateMaterialFor(Batch batch) { 
	     // "calculate material requirement for a production run"
	} 

} 

class Batch {  

private int qty; // 100 units

 private Product product; 

 }

Now, when the technologist and the developer look at the code together, they understand each other. Changing the waste rate means changing one place: the TechnologyMap class.

When these entities are Jmix entities (annotated with @JmixEntity), Jmix Studio automatically generates matching field names, column names, and view labels — not just for ordinary getters and setters, but also for additional, correctly annotated methods tied to business logic. This makes the ubiquitous language truly ubiquitous: in the code, in conversation, and in the user interface.

Bounded Contexts (Core Principle)

A large system should be split into independent modules (contexts), each with its own responsibility and its own internal model. In a manufacturing ERP, the obvious candidates are warehousing, customer management and orders, procurement, production, and so on.

Anti-pattern — everything in one heap

A monolithic Product class holds everything: SKU, selling price, cost price, production process descriptions, stock levels, photographs, and a hundred other attributes. Procurement, production, and sales all depend on it. When accounting asks for a change in cost calculation — say, adding labour-time tracking — the ripple effects are unpredictable. The system becomes brittle.

Better approach — divide and conquer

  • ProductProject context: the Product here is essentially a blueprint. Fields: drawing, TechnologyMap, MaterialCalculation. It knows nothing about stock levels, and it shouldn’t.
  • Production context: ProductionTask is a work order for the shop floor. It references the product, the run size, and a status (in progress, completed, cancelled, etc.). It describes what needs to happen right now.
  • Warehouse context: StockProduct represents a physical object on a shelf. Fields: Material, Product, qty, selfCost, WarehouseSlot. No knowledge of production technology or customers — just quantity and cost.
  • Sales context: ProductPosition is a catalogue entry. Fields: photo, clientDescription, price, ProductLink. No knowledge of cost price — only the selling price.

With this separation, changing a painting technology in ProductProject doesn’t ripple into other contexts. Accounting can revise the cost-calculation algorithm in Warehouse without risking the customer-facing catalogue. The system becomes modular, resilient to change, and far easier to maintain.

Jmix makes it straightforward to build modular (composite) applications, which maps neatly onto bounded contexts. Jmix Studio provides everything you need for managing such projects, from templates to specialised views. There are also convenient tools for managing inter-module dependencies — see the documentation for details.

Aggregates (Supplementary Principle)

An aggregate is a cluster of related objects governed by a single root object. All mutations flow through that root — there is no other way in. This guarantees that business rules and invariants (for example, “you cannot ship items to a customer that have not been manufactured”) are always enforced. Think of an aggregate as a black-box class with no public getters or setters, controllable only through business methods.

Anti-pattern - anyone can change anything

  • A manager edits the price on an already-approved order:
OrderLineItem item = orderLineRepository.findById(lineId);

item.setPrice(new BigDecimal("500")); // Was 700

item.save(); // Financial error: agreed on one price, invoiced another.
  • A discount system sets a 50% discount directly:
order.setTotalDiscount(new BigDecimal("0.5"));

// 50% discount, but the manager's limit is 15%.

Better approach - one entry point, enforced rules

class Order {  

private OrderId id;
    
private OrderStatus status;
    
private Client client;
    
private List<OrderItem> orderItems; // Internal, not exposed
    
private Discount discount;
    
private LocalDateTime fixedPriceDate;

 // No public getters or setters for fields
 
 // === INVARIANTS ===
    
// 1. Prices are locked at the moment of approval
    
// 2. Line items may only be modified while status is DRAFT
    
// 3. Discount must not exceed the manager's limit
    
// 4. Status transitions are strictly controlled
 
// === Public methods: the only way to mutate the aggregate ===
 

public void addPosition(Product product, Qty qty) { 
    if (this.status != OrderStatus.DRAFT) { 
        throw new DomainException("Items can only be changed while the order is a draft."); 
    } 
    Price fixedPrice = priceService.getCurrentPrice(product); 
    this.orderItems.add(new OrderItem(product, qty, fixedPrice)); 
    this.recalculateOrder(); 
} 
 
public void applyDiscount(Discount discount, User manager) { 
    if (discount.greaterThan(manager.getDiscountLimit())) { 
        throw new DomainException("Discount exceeds the manager's authority limit."); 
    } 
    this.discount = new Discount(discount); 
    this.recalculateOrder(); 
} 
 
// Other methods omitted
} 

The code above is illustrative, not a production template.

Some context is worth noting here. DDD and JPA are, frankly, an awkward fit:

  • DDD demands full encapsulation — all changes must go through the aggregate root.
  • JPA in general, and Jmix in particular, rely on data-access interfaces, entity relationships, and similar mechanisms. These often grant access to entities from practically anywhere (views, service beans, repositories), which conflicts with the aggregate pattern.
  • Jmix also generates editing views for entities, which doesn’t help encapsulation either.

This might look like Jmix and DDD are at odds, but that’s not the full picture. Like most methodologies, DDD is a set of guiding principles, not dogma — and aggregates are not even one of its foundational tenets.

Encapsulation and invariant enforcement can still be achieved alongside Jmix’s tooling. For example:

  • Guard invariants at multiple levels: private methods inside the entity, business-method validation in Spring service beans, and persistence-level constraints.
  • Separate the model into read and write layers. This is harder, but remains a viable option.
  • Use Jmix entity events for event-driven validation and auditing to prevent unwanted changes.
  • Delegate mutating operations from standard views to services with strict validation logic — essentially overriding default behaviour and applying validation by contract.

In short, the aggregate pattern is achievable in Jmix, though not to purist standards. Studio’s tooling makes implementing invariant guards less labour-intensive. Pay special attention to Jmix’s composition mechanism and the @Composition annotation. When a relationship is marked as a composition, child entities are saved only when the parent is saved — a natural fit for aggregate semantics. Details are in the documentation.

Entities and Value Objects (Core Principle)

The core idea here is that we model real-world objects and processes, not database table structures. Database design has its own specialists. In DDD, we work with two fundamental building blocks — Entities and Value Objects — and they are not the same thing.

An Entity has identity and a lifecycle. Each instance is unique and tracked throughout its existence. Its identifier matters more than its contents.

Examples:

  • Order with ID 547 — customer order number 547. Even if its contents or delivery address change, it remains the same order.
  • MaterialBatch with ID “FAN-034” dated 30 October 2025 — a batch of plywood with a specific cost.

A Value Object is defined entirely by its attributes. It has no identity. If all fields match, two instances are interchangeable and may be freely copied.

Examples:

  • DeliveryAddress (15 High Street, Flat 34) — if two orders share the same address, it is the same address.
  • MoneyAmount — twenty pounds is always twenty pounds.

The takeaway is straightforward: model production entities such as Task, Order, and Batch with unique identity, and model characteristics such as Sizes, Weight, Price, and Address as immutable value objects. The result is a system that behaves predictably — like a real production line, not a collection of database tables.

Jmix’s data layer is built on JPA, so all entities are ORM entities. But for DDD, the distinction matters:

  • A DDD Entity is roughly analogous to a noun — it doesn’t imply a specific implementation. In Jmix, this maps well to a JPA entity with an emphasis on identity and behaviour. @JmixEntity is a natural fit.

A DDD Value Object can be implemented as:

  • An @Embeddable class (stored in the parent’s table).
  • A non-JPA class used only in business logic — a DTO or plain POJO.
  • Custom data types with straightforward UI support, which are in effect value-object implementations.

One common DDD technique is to create a separate entity or class with equals()/hashCode() based on field values. In Jmix, this should be avoided, because Jmix’s build-time entity enhancement mechanism relies on its own identity handling.

Core Domain — Isolating the Domain Layer (Supplementary principle)

In DDD, the domain layer is sacred. It contains pure business logic and must not depend on databases, external services, frameworks, or UI implementations. Those “technical” concerns belong in separate layers (Infrastructure, Application). We’ll skip worked examples here and focus on the theoretical benefits:

  • Testability — domain logic can be tested without a database or a web server.
  • Replaceability — you can swap PostgreSQL for another database without touching business logic.
  • Focus — a developer working on the domain layer thinks about the business, not about plumbing.

Jmix inherently separates an application into layers:

  • UI layer — view controllers, view descriptors, menus, and message bundles.
  • Service layer — your Spring beans.
  • Entity layer — the entity model. Note that this is not a pure domain model in the DDD sense, which is an important distinction.
  • Data Access layer — if you need or prefer one, you can create a dedicated repository layer.

Now for the pragmatic bit: dogma versus trade-offs. A pure domain model in Java means plain Java code — no third-party annotations, no JPA, and certainly no Jmix. That sounds elegant, and all the benefits listed above apply. But making POJO classes do real work means dealing with a few practicalities:

  • Data persistence — you probably want a database, not just in-memory storage. That requires additional layers, from raw JDBC up to JPA.
  • Data processing — a domain model exists to serve business tasks, not to look good on paper. Data needs to be mutated, validated, passed between modules, and sent to external services. At the domain-model level this looks simple; the implementation may take longer than expected.

A pure domain model is an appealing ideal, but it is just that — an ideal. Jmix offers a pragmatic, modified DDD approach built around a Unified Data Model, which saves significant development time by compressing the data-layer stack. You don’t need domain-to-persistence mappers, JPA wrappers, or custom REST controllers. If you can set aside purist concerns, this approach delivers strong results for enterprise applications with conventional web UIs.

Domain Events via the Event Model (Supplementary principle)

DDD recommends using domain events. Jmix ships with a robust event system covering both standard JPA entity-lifecycle events and Jmix-specific events. Event listeners let you detect and react to entity changes before or after a transaction commits, and they carry information about the type of change and other useful context. For full details, see the entity events documentation.

Using Jmix in a DDD-Aligned Workflow

Rapid Prototyping with a Reasonably Clean Domain

With classical DDD, a lot of time goes into standing up infrastructure — database connectivity, a minimal UI, and so on — before you see a working application.

With Jmix, you define domain entities (following DDD principles), and Jmix automatically generates:

  • Database tables
  • CRUD views
  • A REST API (if needed)

For example, suppose you model ProductionTask as an aggregate. Within minutes you get:

  • A creation view (Jmix StandardDetailView)
  • A list view (Jmix StandardListView) with filtering, pagination, and grouping
  • An editing form within the same detail view
  • REST endpoints after adding the REST API add-on, which takes about fifteen seconds to install and gives you endpoints straight away

Later, you can replace the generated views with custom ones, or drop them entirely and build a front end in modern JavaScript — the domain model stays intact. And “a few minutes” is not a figure of speech. Jmix Studio’s tooling enables remarkable speed:

  • View wizards for scaffolding list and detail views from the model in a few clicks
  • An entity designer for quickly adding validation, localisation, and more
  • A role designer for visual access-control management with role-hierarchy support

Beyond Studio itself, Jmix provides a large library of ready-to-use add-ons — from report generation to business-process automation. Here are a few highlights.

Visual Designer for Complex Business Processes

For complex systems such as the ERP described above, business processes are essential. Jmix covers this ground well.

The Jmix BPM add-on lets you:

  • Visually design processes (much like a BPMS) directly within the running application — and use Jmix views as process forms.
  • Integrate those processes with your domain model.
  • Launch and monitor execution with a convenient UI for both processes and user tasks.

Ready-Made Components for Common Business Tasks

  • Printed forms (orders, invoices, reports, etc.) — the Reports add-on
  • Audit trail (who changed what, and when, in any aggregate or entity) — the Audit add-on
  • REST DataStore — another take on module and context separation; popular with the community

Integration with External Systems

Manufacturing rarely operates in isolation. Common requirements include exporting orders to an accounting system, pushing data to finance platforms, and integrating with marketplaces.

Jmix offers:

  • A built-in REST API (mentioned above)
  • Integration with Kafka and RabbitMQ for asynchronous communication between contexts — the full power of Spring Boot is at your disposal
  • Scheduled tasks via the Quartz add-on

Conclusion

Domain-Driven Design is a compelling pattern for building large, complex enterprise applications. Jmix is a platform built for exactly that class of systems. Combining DDD thinking with Jmix’s capabilities lets you build things right and build them fast, affordably, on a transparent and modern technology stack.

Most importantly, a project started on Jmix benefits from a battle-tested architecture that can evolve as functionality grows, without costly redesigns.

To sum up:

  • DDD and Jmix are not opposites — they are a sensible combination.
  • Use DDD principles to develop a deep understanding of the business and to build the right architecture.
  • Use Jmix to deliver predictable results quickly, avoid writing the same code twice, and stay out of infrastructure rabbit holes.
  • Balance is the key: take from DDD the principles that yield the greatest benefit for the least overhead (Ubiquitous Language, Bounded Contexts, Value Objects), and take from Jmix the development speed, Studio tooling, and the ecosystem of ready-made components and add-ons.

Jmix itself is a free, open-source framework. Applications you build with it are entirely yours. Most add-ons can be used without a Jmix Studio subscription.

You can try the full Jmix Studio experience free for 28 days here. Students get free access for the duration of their studies, upon request. Studio is also free for small projects.

Jmix is an open-source platform for building enterprise applications in Java

Recommended Reading