Why We Chose Django + Flutter for Our Marketplace Platform (And What We Learned)
Originally published at niqow.ir
When we started building Niqow, a software development team specializing in e-commerce and platform solutions, we had a simple question:
What's the right tech stack for a multi-vendor marketplace that needs to scale from 10 sellers to 10,000?
We evaluated several options — Node.js, Laravel, and even Golang for the backend. In the end, we chose Django for the backend and Flutter for the mobile app. This is a practical look at why, and what we learned building a real marketplace platform on this stack.
The Requirements
Before choosing a stack, we defined what the platform actually needed to do:
- Multi-tenancy: Each seller has isolated data, but shares the same database
- Real-time messaging: Buyers and sellers need to chat in real time
- Video consultations: Some sellers offer consultation sessions
- Commission-based payments: The platform takes a cut from every transaction
- Multi-platform access: Web + Android + iOS
This is not a simple CRUD app. It's a system where data isolation, transaction integrity, and real-time communication all matter.
Why Django for the Backend
We chose Django for three specific reasons:
1. Built-in Multi-Tenancy Patterns
Django's ORM makes it straightforward to implement row-level multi-tenancy:
class Seller(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
shop_name = models.CharField(max_length=200)
commission_rate = models.DecimalField(max_digits=5, decimal_places=2)
class Product(models.Model):
seller = models.ForeignKey(Seller, on_delete=models.CASCADE)
name = models.CharField(max_length=200)
class Meta:
indexes = [
models.Index(fields=['seller', 'is_active']),
]
Every query is automatically scoped by seller. No shared data leaks.
2. Django Channels for Real-Time Features
For the built-in messaging system between buyers and sellers, we used Django Channels with WebSocket support:
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_id = self.scope['url_route']['kwargs']['room_id']
await self.channel_layer.group_add(
f"chat_{self.room_id}",
self.channel_name
)
await self.accept()
async def receive(self, text_data):
message = json.loads(text_data)
await self.channel_layer.group_send(
f"chat_{self.room_id}",
{'type': 'chat_message', 'message': message['text']}
)
Real-time messaging without leaving the Django ecosystem.
3. DRF for API Development
Django REST Framework gave us a consistent way to expose APIs to both the web frontend and the Flutter mobile app:
class ProductViewSet(viewsets.ModelViewSet):
serializer_class = ProductSerializer
permission_classes = [IsSellerOrReadOnly]
def get_queryset(self):
if self.request.user.is_seller:
return Product.objects.filter(seller=self.request.user.seller)
return Product.objects.filter(is_active=True)
One API, three clients (web, Android, iOS).
Why Flutter for Mobile
We chose Flutter over React Native for three reasons:
- Single codebase for Android and iOS — critical for a small team
- Native performance — important for real-time chat and video
- Consistent UI — the same experience across platforms
The Flutter app communicates with the Django backend via REST APIs and WebSockets.
The Architecture
Here's the overall architecture:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Web App │ │ Flutter │ │ Flutter │
│ (Next.js) │ │ (Android) │ │ (iOS) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└──────────────────┼──────────────────┘
│
┌─────▼─────┐
│ Django │
│ + DRF │
│ + Channels│
└─────┬─────┘
│
┌───────────┼───────────┐
│ │ │
┌─────▼────┐ ┌────▼────┐ ┌────▼────┐
│PostgreSQL│ │ Redis │ │ MinIO │
└──────────┘ └─────────┘ └─────────┘
What We Learned
Lesson 1: Multi-Tenancy Is Harder Than It Looks
Row-level multi-tenancy works, but you have to be disciplined. Every query must be scoped by tenant. We solved this with a custom manager:
class TenantManager(models.Manager):
def for_seller(self, seller):
return self.filter(seller=seller)
Lesson 2: Real-Time Chat Needs Careful Design
Django Channels works, but scaling WebSockets requires a message broker. We used Redis as the channel layer backend:
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {"hosts": [("redis", 6379)]},
},
}
Lesson 3: Commission Logic Should Be a Service, Not a Signal
We initially used Django signals to calculate commissions. Big mistake — signals are hard to test and debug. We refactored into an explicit service:
class CommissionService:
@staticmethod
def calculate(order):
commission = order.total * order.seller.commission_rate / 100
Commission.objects.create(
seller=order.seller,
order=order,
amount=commission,
status='pending'
)
return commission
Now it's testable, traceable, and deterministic.
Lesson 4: Choose Boring Technology
We didn't use Kubernetes. We didn't use microservices. We used Docker Compose and a monolith. This wasn't a limitation — it was a decision that let us ship faster and debug easier.
The Result
The platform — which we call MartLink — now handles:
- Multi-vendor product management
- Real-time buyer-seller chat
- Video consultation sessions
- Automated commission settlement
- Mobile apps for Android and iOS
It's not the most elegant architecture, but it works, it scales, and it ships.
Conclusion
If you're building a marketplace platform, here's what I'd suggest:
- Django + DRF + Channels is a solid backend foundation
- Flutter is a great choice for multi-platform mobile
- PostgreSQL + Redis + MinIO covers 95% of your infrastructure needs
- Monolith first, microservices later — don't over-engineer
You can see what we built at Niqow. If you're working on a similar platform, feel free to reach out — we're always happy to talk architecture.
What stack are you using for your marketplace platform? Share your experience in the comments.
Top comments (0)