Good design does not end when the architecture diagram is finished. A team still has to turn requirements, domain knowledge, and system boundaries into working code.
This is where practical design and development techniques help. Domain Driven Design gives structure to the model. Test-driven development keeps implementation grounded in executable checks. Behavior Driven Development connects business behavior to tests. User story mapping helps the team decide what to build first.
Used together, these practices help developers avoid random implementation, oversized service classes, unclear requirements, and releases that contain work but little user value.
The Problem
A common failure mode in Java applications is moving too quickly from requirements to code.
The team collects features, draws architecture diagrams, creates database tables, and then starts building service classes. At first, this feels productive. Over time, the model becomes hard to understand.
Business concepts are hidden behind generic names. Objects only contain fields. Rules are spread across services. Tests focus on implementation details instead of behavior. Planning becomes a long backlog with no clear product shape.
The result is software that works for the first release but becomes harder to evolve with every change.
Domain Driven Design
Domain Driven Design, usually called DDD, is a modeling approach for complex business domains. Its goal is to represent the real problem in software, not just to wrap database tables with Java classes.
DDD is useful when the business rules are the difficult part of the system. It may be too heavy for small CRUD applications, but many of its ideas are still useful even when adopted partially.
The first warning sign DDD tries to avoid is the anemic domain model.
class Payment {
private String id;
private String sender;
private String recipient;
private double amount;
String getId() {
return id;
}
void setId(String id) {
this.id = id;
}
}
class PaymentService {
void authorize(Payment payment) {
validate(payment);
save(payment);
}
void validate(Payment payment) {
}
void save(Payment payment) {
}
}
This design creates objects that mainly carry data. Most behavior lives somewhere else. For simple database screens, this can be acceptable, but in a complex domain, it pushes the system toward procedural code hidden inside object-oriented syntax.
DDD encourages richer models. Important domain objects should contain behavior that naturally belongs to them.
class Payment {
private final PaymentId id;
private final AccountId sender;
private final AccountId recipient;
private final Money amount;
private PaymentStatus status;
Payment(PaymentId id, AccountId sender, AccountId recipient, Money amount) {
this.id = id;
this.sender = sender;
this.recipient = recipient;
this.amount = amount;
this.status = PaymentStatus.CREATED;
}
void authorize() {
if (status != PaymentStatus.CREATED) {
throw new IllegalStateException("Payment cannot be authorized");
}
status = PaymentStatus.AUTHORIZED;
}
}
The payment now owns a rule about its own state. A service may still coordinate a use case, but the domain object is no longer just a bag of fields.
Ubiquitous Language
DDD starts with language.
A team needs a shared vocabulary between developers and domain experts. This shared vocabulary is called ubiquitous language. It should appear in conversations, requirements, diagrams, tests, class names, method names, and documentation.
If the business talks about sender, recipient, payment, authorization, transaction, and dispute, then the code should use those same ideas consistently. Vague terms such as data manager, processor, helper, or item usually hide business meaning.
Ubiquitous language is not created by one document. It is created by working together, asking what words mean, correcting misunderstandings, and updating the model when the team learns something new.
Layer the Application
DDD commonly uses a layered architecture to protect the domain model from technical details.
Presentation Layer
Handles user interaction and external API input
Application Layer
Coordinates use cases and calls the domain
Domain Layer
Contains business objects, state, and behavior
Infrastructure Layer
Handles databases, external systems, and technical glue
The presentation layer deals with screens, APIs, or machine-to-machine calls.
The application layer coordinates the flow. It should not become the place where every business rule is stored.
The domain layer contains the business model. This is where entities, value objects, services, and aggregates belong.
The infrastructure layer connects the model to databases, queues, legacy systems, and external services.
This separation matters because a change in persistence, user interface, or integration technology should not force unnecessary changes in the core domain model.
Model with Entities, Value Objects, Services, and Aggregates
An entity is defined by identity. A user, account, or payment usually has a life cycle and identity that remains meaningful even when fields change.
A value object is defined by its values. It usually has no identity and should be immutable where possible.
final class Money {
private final double amount;
private final String currency;
Money(double amount, String currency) {
this.amount = amount;
this.currency = currency;
}
double amount() {
return amount;
}
String currency() {
return currency;
}
}
A service is useful when behavior does not naturally belong to one entity or value object. For example, a peer-to-peer payment may involve a sender, a recipient, a payment, and external validation. A domain service can express that operation without forcing the behavior into the wrong object.
An aggregate groups objects that must remain consistent together. It has a root entity. External code should modify the aggregate through that root instead of directly changing internal objects.
Use Factories and Repositories
DDD also includes patterns for common supporting tasks.
A factory helps create complex objects or aggregates in one place. This avoids scattered construction logic.
class PaymentFactory {
Payment create(AccountId sender, AccountId recipient, Money amount) {
return new Payment(PaymentId.newId(), sender, recipient, amount);
}
}
A repository hides persistence details behind domain-friendly operations.
interface PaymentRepository {
Payment findById(PaymentId id);
void save(Payment payment);
}
The domain does not need to know whether the repository uses SQL, a web service, or another storage mechanism. That belongs to infrastructure.
Bounded Contexts
Large systems rarely fit one perfect model. The same word may mean different things in different parts of the business.
A customer in a customer relationship system may not be the same concept as a user in a payment system. Trying to force both into one model can create confusion.
DDD solves this with bounded contexts. A bounded context is a boundary where one model and one vocabulary are valid.
Payment Context
Payment
Sender
Recipient
Authorization
Profile Context
Customer
Contact Details
Preferences
Integration
Payment Context -> Translation Layer -> Profile Context
Different bounded contexts should communicate through explicit interfaces. They should not freely share internal objects. When models differ, a translation layer can prevent one model from corrupting another.
Test Driven Development
Test Driven Development, or TDD, changes the order of implementation.
Instead of writing code first and tests later, the developer writes a failing test first. Then the developer writes the smallest useful implementation to make the test pass. After that, the code can be improved safely.
class PaymentTest {
void createdPaymentCanBeAuthorized() {
Payment payment = PaymentFactory.samplePayment();
payment.authorize();
assertEquals(PaymentStatus.AUTHORIZED, payment.status());
}
}
The value of TDD is not that tests magically create good design. The value is that tests force the developer to define expected behavior before implementation grows.
TDD also pushes the code toward smaller, more testable units. If something is painful to test, it may be too tightly coupled or doing too many things.
Behavior Driven Development
Behavior Driven Development, or BDD, extends the test-first idea by describing behavior in business language.
BDD often uses a user story structure:
As a registered user
I want to make a payment to another user
So that I can transfer money
Acceptance criteria can be written with Given, When, Then:
Given I am a registered user
And I am logged in
And I enter a valid recipient
And I enter a valid amount
When I confirm the payment
Then, a payment transaction is created
And a notification is sent
This format is useful because business people can understand it, and developers can translate it into automated tests. It also keeps the team focused on behavior instead of only implementation details.
Comparing DDD, TDD, and BDD
DDD, TDD, and BDD are not competitors.
DDD helps shape the model and boundaries of the application.
TDD helps developers implement code with executable feedback.
BDD helps connect business behavior to testable scenarios.
A team can use DDD to define entities, services, aggregates, and bounded contexts. Then it can use BDD to describe expected behavior. Finally, it can use TDD while implementing the code that satisfies those behaviors.
User Story Mapping
A backlog can become a flat list of disconnected tasks. User story mapping gives it structure.
At the top, place user activities or user stories in the order the user experiences them. Under each activity, list tasks. Under each task, list subtasks or details.
Registration Login Payment
Create user Sign in Select recipient
Validate data Check password Search recipient
Store profile Start session Enter amount
Confirm payment
Send notification
This gives the team a product map instead of only a task list. It becomes easier to see what belongs together and what can be released first.
Value Slicing and MVP
Value slicing means choosing a thin but meaningful set of features for a release. The goal is not to complete one technical layer at a time. The goal is to deliver something useful end-to-end.
An MVP, or Minimum Viable Product, should contain enough functionality to represent the product and gather feedback, but not so much that the team spends too long before learning from users.
For example, the first release of a payment application might include:
- create a user
- sign in
- search for a recipient
- create a payment
Later releases can add social login, lost password handling, payment disputes, and additional notifications.
This approach reduces risk. The team gets feedback earlier and can adjust the product direction before investing too much effort in secondary features.
Practical Workflow
A useful workflow for a Java team can look like this:
- Discuss the business domain with domain experts.
- Build a shared vocabulary and use it everywhere.
- Identify entities, value objects, services, aggregates, and bounded contexts.
- Keep infrastructure concerns outside the domain layer.
- Write user stories that explain business value.
- Add acceptance criteria with Given, When, Then.
- Write tests before or alongside implementation.
- Slice the story map into small releases.
- Release a meaningful MVP early.
- Refine the model as the team learns more.
Common Mistakes
The first mistake is calling every design DDD while still using an anemic model. If objects contain no behavior, the domain is not rich.
The second mistake is letting database tables define the model. Persistence is important, but the domain should express business meaning first.
The third mistake is putting too much business logic into the application layer. Coordination belongs there, but core rules belong in the domain.
The fourth mistake is writing tests that only check implementation details. Tests should also protect business behavior.
The fifth mistake is treating the backlog as the product. A backlog is a list. A story map shows how the user receives value.
Checklist
Before building the next feature, check the following:
- The team uses the same domain terms in meetings and code.
- Important concepts have meaningful names.
- Entities have clear identities.
- Value objects are immutable where practical.
- Business behavior is placed near the concept that owns it.
- Services are used for cross-object behavior, not as dumping grounds.
- Aggregates define clear consistency boundaries.
- Bounded contexts are explicit.
- Acceptance criteria describe observable behavior.
- Releases deliver user value, not only technical progress.
Conclusion
Best practices for design and development are strongest when they connect architecture, business behavior, and code.
DDD helps build a model that reflects the real domain. TDD makes implementation safer through executable feedback. BDD gives business-readable behavior specifications. User story mapping and value slicing help teams release meaningful increments instead of waiting for a perfectly complete product.
Used together, these techniques help Java teams build software that is easier to discuss, easier to test, and easier to evolve.