This solution demonstrates a small, extensible fruit pricing system built using C# and .NET 10.
The application calculates the total cost of fruit orders while supporting multiple pricing models and promotional pricing rules. The design follows SOLID principles, Clean Architecture concepts, and common enterprise development practices.
Each fruit contains:
- Name
- Base Price
- Pricing Strategy
Example:
| Fruit | Base Price | Pricing Method |
|---|---|---|
| Apple | $2.00 | Per Kilogram |
| Banana | $0.30 | Per Item |
| Cherry | $5.00 | Per Kilogram with 10% discount above 2kg |
Example:
Apple
1.5kg × $2.00
= $3.00
Example:
Banana
6 × $0.30
= $1.80
Example:
Cherry
3kg × $5.00
= $15.00
10% Discount
= $13.50
The system supports orders containing multiple fruit types and calculates a single total price.
Example:
Apple 1.5kg
Banana 6 items
Cherry 3kg
Total = $18.30
FruitShop.sln
│
├── FruitShop.Domain
│
├── FruitShop.Application
│
├── FruitShop.Infrastructure
│
├── FruitShop.Console
│
└── FruitShop.Tests
Contains business rules and core domain models.
Fruit
Order
OrderLine
IPricingStrategy
PerKilogramPricingStrategy
PerItemPricingStrategy
DiscountedKilogramPricingStrategy
The Domain layer contains no infrastructure concerns and no dependency injection.
Contains application orchestration logic.
IFruitFactory
FruitFactory
IPricingStrategyFactory
PricingStrategyFactory
IPricingStrategyResolver
PerKilogramPricingResolver
PerItemPricingResolver
DiscountedKilogramPricingResolver
IOrderPricingService
PricingEngine
The Application layer coordinates the domain model and business workflows.
Contains external concerns.
JsonFruitCatalog
fruits.json
Infrastructure is responsible for loading fruit definitions from configuration.
Application entry point.
Responsibilities:
- Configure dependency injection
- Build the application host
- Execute the pricing use case
No business logic exists in this layer.
Contains unit tests for:
Pricing Strategies
Pricing Strategy Resolvers
Pricing Strategy Factory
Fruit Factory
Pricing Engine
The Strategy Pattern is used to encapsulate pricing algorithms.
Different fruits may use completely different pricing rules.
Without Strategy:
if (fruit == "Apple")
{
}
else if (fruit == "Banana")
{
}This quickly becomes difficult to maintain.
With Strategy:
IPricingStrategyEach pricing rule becomes an independent implementation.
Benefits:
- Open/Closed Principle
- Easy testing
- Easy extension
- Clear separation of responsibilities
The Factory Pattern is used to create fruit instances and pricing strategies.
Responsible for creating Fruit domain objects.
Responsible for creating pricing strategies from fruit configuration.
Benefits:
- Centralized object creation
- Reduced coupling
- Easier maintenance
- Better testability
Dependency Injection is used throughout the application.
Benefits:
- Loose coupling
- Easier unit testing
- Better maintainability
- Adherence to SOLID principles
The system avoids large switch statements by using resolver registration.
Example:
PricingType.PerKg
↓
PerKilogramPricingResolver
↓
PerKilogramPricingStrategy
Adding a new pricing strategy requires creating:
NewPricingStrategy
NewPricingResolver
and registering it in Dependency Injection.
Existing code remains unchanged.
Fruit definitions are stored in:
fruits.json
Example:
[
{
"name": "Apple",
"basePrice": 2.00,
"pricingType": "PerKg"
},
{
"name": "Banana",
"basePrice": 0.30,
"pricingType": "PerItem"
},
{
"name": "Cherry",
"basePrice": 5.00,
"pricingType": "DiscountedKg",
"discountThreshold": 2,
"discountPercentage": 0.10
}
]Fruit names and property names are processed case-insensitively.
- .NET 10 SDK or later
Verify installation:
dotnet --versiondotnet builddotnet run --project FruitShop.ConsoleExpected output:
Fruit Shop
----------
Apple - Qty: 1.5
Banana - Qty: 6
Cherry - Qty: 3
Total: $18.30
Run all tests:
dotnet testThe solution uses:
- xUnit
- Moq
- FluentAssertions
Tests follow the Arrange / Act / Assert pattern.
Example:
[Fact]
public void CalculatePrice_ShouldReturnCorrectPrice()
{
// Arrange
var strategy = new PerKilogramPricingStrategy();
// Act
var result = strategy.CalculatePrice(2m, 5m);
// Assert
result.Should().Be(10m);
}Add a new entry to:
fruits.json
Example:
{
"name": "Orange",
"basePrice": 3.00,
"pricingType": "PerKg"
}No code changes required.
Create a new strategy:
public sealed class BuyOneGetOnePricingStrategy
: IPricingStrategy
{
}Create a resolver:
public sealed class BuyOneGetOnePricingResolver
: IPricingStrategyResolver
{
}Register it in Dependency Injection.
No existing code must be modified.
Create:
SeasonalDiscountPricingStrategy
SeasonalDiscountPricingResolver
and configure discount rules through JSON or a future database-backed catalog.
For a production-grade implementation, the following improvements could be added:
- Money Value Object
- Currency support
- Rich domain validation
- Database-backed fruit catalog
- Distributed caching
- External pricing service integration
- Structured logging with Serilog
- OpenTelemetry tracing
- Metrics collection
- Integration tests
- Contract tests
- Mutation testing
- BenchmarkDotNet benchmarks
- Catalog caching
- Parallel order processing
The solution was built around the following principles:
- Single Responsibility Principle
- Open/Closed Principle
- Dependency Inversion Principle
- Separation of Concerns
- Clean Architecture
- Testability
- Extensibility
The resulting design allows new fruits, pricing models, and discount strategies to be added with minimal changes to existing code while maintaining a high level of test coverage and maintainability.