Project Structure
Panduan lengkap struktur folder dan file untuk memahami organisasi codebase.📁 Root Structure
project-root/
├── backend/ # Go services
├── frontend/ # Flutter app
├── docs/ # Documentation (Mintlify)
├── docker-compose.yml # Dev services
├── Taskfile.yml # Task definitions
├── .env.example # Environment template
├── .gitignore
├── README.md
└── observability/ # LGTM configs
🔧 Backend Structure (Go)
backend/
├── cmd/
│ └── api/ # API Gateway
│ └── main.go
│
├── internal/
│ ├── route/ # 7-Domain Route Architecture
│ │ ├── route.go # Main router entry point
│ │ ├── v1/ # API V1 (Backward Compatible)
│ │ │ ├── route_registry.go
│ │ │ ├── core/
│ │ │ │ └── core_route.go
│ │ │ ├── ops/
│ │ │ │ ├── transaction_route.go
│ │ │ │ ├── inventory_route.go
│ │ │ │ └── pos_route.go
│ │ │ ├── support/
│ │ │ │ ├── finance_route.go
│ │ │ │ ├── hr_route.go
│ │ │ │ └── crm_route.go
│ │ │ ├── webhook/
│ │ │ │ └── webhook_route.go
│ │ │ ├── analytics/
│ │ │ ├── holding/
│ │ │ └── global/
│ │ └── v2/ # API V2 (Enhanced)
│ │ ├── route_registry.go
│ │ ├── core/
│ │ ├── ops/
│ │ ├── support/
│ │ ├── governance/
│ │ ├── analytics/
│ │ ├── holding/
│ │ └── global/
│ │
│ ├── domains/ # Business domains (services)
│ │ ├── auth/ # Auth domain
│ │ │ ├── handler/
│ │ │ ├── service/
│ │ │ ├── repository/
│ │ │ ├── model/
│ │ │ └── dto/
│ │ ├── transaction/
│ │ ├── inventory/
│ │ ├── pos/
│ │ ├── webhook/
│ │ └── ...
│ │
│ ├── middleware/ # HTTP middleware
│ │ ├── auth_middleware.go
│ │ ├── rbac_middleware.go
│ │ ├── audit_middleware.go
│ │ └── ...
│ │
│ ├── store/ # Handler registry
│ │ └── handler.go
│ │
│ └── shared/ # Shared packages
│ ├── config/ # Configuration
│ ├── database/ # DB connections
│ ├── logger/ # Logging
│ ├── validator/ # Validation
│ ├── errors/ # Error handling
│ └── utils/ # Utilities
│
├── pkg/ # Public packages
│ ├── common/
│ ├── utils/
│ └── response/
│
├── config/ # Config files
│ ├── api/
│ │ ├── endpoints-registry-v1.yaml
│ │ └── endpoints-registry-v2.yaml
│ ├── rbac/
│ │ ├── policy.csv
│ │ └── sod_rules.yaml
│ └── ...
│
├── migrations/ # Database migrations
│ ├── atlas/
│ │ └── schema/
│ └── ...
│
├── tests/ # Tests
│ ├── integration/
│ ├── e2e/
│ └── fixtures/
│
├── .air.toml # Air config (live reload)
├── go.mod
├── go.sum
├── Dockerfile
└── README.md
Backend File Examples
- main.go
- handler.go
- service.go
- repository.go
// cmd/api/main.go
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"github.com/your/project/internal/shared/config"
"github.com/your/project/internal/shared/database"
"github.com/your/project/internal/shared/logger"
)
func main() {
// Load config
cfg, err := config.Load()
if err != nil {
log.Fatal(err)
}
// Setup logger
logger := logger.New(cfg.Log)
// Connect to database
db, err := database.NewMySQL(cfg.Database)
if err != nil {
logger.Fatal("failed to connect database", err)
}
defer db.Close()
// Setup server
server := setupServer(cfg, db, logger)
// Graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
go func() {
if err := server.Start(); err != nil {
logger.Fatal("server error", err)
}
}()
<-quit
logger.Info("shutting down server...")
if err := server.Shutdown(context.Background()); err != nil {
logger.Fatal("server shutdown error", err)
}
}
// internal/auth/handler/auth_handler.go
package handler
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/your/project/internal/auth/dto"
"github.com/your/project/internal/auth/service"
"github.com/your/project/pkg/response"
)
type AuthHandler struct {
authService service.AuthService
}
func NewAuthHandler(authService service.AuthService) *AuthHandler {
return &AuthHandler{authService: authService}
}
func (h *AuthHandler) Login(c echo.Context) error {
var req dto.LoginRequest
if err := c.Bind(&req); err != nil {
return response.Error(c, http.StatusBadRequest, "invalid request")
}
if err := c.Validate(&req); err != nil {
return response.ValidationError(c, err)
}
token, err := h.authService.Login(c.Request().Context(), &req)
if err != nil {
return response.Error(c, http.StatusUnauthorized, err.Error())
}
return response.Success(c, http.StatusOK, token)
}
// internal/auth/service/auth_service.go
package service
import (
"context"
"errors"
"github.com/your/project/internal/auth/dto"
"github.com/your/project/internal/auth/repository"
"github.com/your/project/pkg/hash"
"github.com/your/project/pkg/jwt"
)
type AuthService interface {
Login(ctx context.Context, req *dto.LoginRequest) (*dto.TokenResponse, error)
Register(ctx context.Context, req *dto.RegisterRequest) error
}
type authService struct {
userRepo repository.UserRepository
jwt jwt.JWT
}
func NewAuthService(userRepo repository.UserRepository, jwt jwt.JWT) AuthService {
return &authService{
userRepo: userRepo,
jwt: jwt,
}
}
func (s *authService) Login(ctx context.Context, req *dto.LoginRequest) (*dto.TokenResponse, error) {
user, err := s.userRepo.FindByEmail(ctx, req.Email)
if err != nil {
return nil, errors.New("invalid credentials")
}
if !hash.CheckPassword(req.Password, user.Password) {
return nil, errors.New("invalid credentials")
}
token, err := s.jwt.Generate(user.ID, user.Role)
if err != nil {
return nil, err
}
return &dto.TokenResponse{
AccessToken: token,
TokenType: "Bearer",
}, nil
}
// internal/auth/repository/user_repository.go
package repository
import (
"context"
"github.com/your/project/internal/auth/model"
"gorm.io/gorm"
)
type UserRepository interface {
FindByEmail(ctx context.Context, email string) (*model.User, error)
Create(ctx context.Context, user *model.User) error
}
type userRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) FindByEmail(ctx context.Context, email string) (*model.User, error) {
var user model.User
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error
return &user, err
}
func (r *userRepository) Create(ctx context.Context, user *model.User) error {
return r.db.WithContext(ctx).Create(user).Error
}
🧩 7-Domain Route Architecture
MStore Backend menggunakan 7-Domain Driven Architecture untuk route organization:Domain Hierarchy
CORE (Foundation)
↓
OPS (Operational)
↓
SUPPORT (Backoffice)
↓
GOVERNANCE (Control)
↓
ANALYTICS (Insight)
↓
HOLDING (Multi-Entity)
↓
GLOBAL (Infrastructure)
V1 Routes (Backward Compatible)
| Domain | Endpoints | Status |
|---|---|---|
| CORE | 21 | ✅ Auth, User, Merchant, Branches, Roles |
| OPS | 37 | ✅ Stok, Transactions, POS, Inventory |
| SUPPORT | 0 | 📋 Placeholder (Finance, HR, CRM) |
| WEBHOOK | 1 | ✅ Xendit Payments |
V2 Routes (Enhanced)
| Domain | Endpoints | Status |
|---|---|---|
| CORE | 7 | ✅ Auth, Role, System |
| OPS | 20 | ✅ POS, Sales, Procurement, Inventory |
| SUPPORT | 20 | ✅ Finance, HR, CRM, Marketing, CS |
| GOVERNANCE | 7 | ✅ Audit, Dashboard, Holding, Global |
| ANALYTICS | 6 | ✅ Dashboard, BI, Forecast |
| HOLDING | 10 | ✅ Entity, Consolidation, Intercompany |
| GLOBAL | 12 | ✅ IAM, Security, Data Governance, ESG |
Adding New Routes
- Create domain folder:
internal/route/v1/your-domain/ - Create route file:
your_domain_route.go - Update
route_registry.gowith import and call - Build and test
📱 Frontend Structure (Flutter)
frontend/
├── lib/
│ ├── main.dart
│ │
│ ├── core/ # Core functionality
│ │ ├── config/ # App configuration
│ │ │ ├── env.dart
│ │ │ └── theme.dart
│ │ ├── constants/ # Constants
│ │ │ ├── api_constants.dart
│ │ │ └── app_constants.dart
│ │ ├── di/ # Dependency injection
│ │ │ └── injection.dart
│ │ ├── network/ # Network layer
│ │ │ ├── dio_client.dart
│ │ │ ├── interceptors/
│ │ │ └── api_response.dart
│ │ ├── router/ # Navigation
│ │ │ └── app_router.dart
│ │ └── utils/ # Utilities
│ │ ├── logger.dart
│ │ └── validators.dart
│ │
│ ├── features/ # Feature modules
│ │ ├── auth/
│ │ │ ├── data/
│ │ │ │ ├── models/
│ │ │ │ ├── datasources/
│ │ │ │ └── repositories/
│ │ │ ├── domain/
│ │ │ │ ├── entities/
│ │ │ │ ├── repositories/
│ │ │ │ └── usecases/
│ │ │ └── presentation/
│ │ │ ├── pages/
│ │ │ ├── widgets/
│ │ │ └── providers/
│ │ │
│ │ ├── home/
│ │ │ ├── data/
│ │ │ ├── domain/
│ │ │ └── presentation/
│ │ │
│ │ ├── product/
│ │ │ ├── data/
│ │ │ ├── domain/
│ │ │ └── presentation/
│ │ │
│ │ └── order/
│ │ ├── data/
│ │ ├── domain/
│ │ └── presentation/
│ │
│ └── shared/ # Shared widgets & utils
│ ├── widgets/
│ │ ├── buttons/
│ │ ├── inputs/
│ │ ├── cards/
│ │ └── dialogs/
│ ├── extensions/
│ └── mixins/
│
├── test/ # Tests
│ ├── unit/
│ ├── widget/
│ └── integration/
│
├── assets/ # Assets
│ ├── images/
│ ├── icons/
│ └── fonts/
│
├── pubspec.yaml
├── analysis_options.yaml
└── README.md
Flutter File Examples
- main.dart
- dio_client.dart
- provider.dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/config/theme.dart';
import 'core/di/injection.dart';
import 'core/router/app_router.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Setup dependencies
await setupDependencies();
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
return MaterialApp.router(
title: 'Windsurf App',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
routerConfig: router,
);
}
}
// lib/core/network/dio_client.dart
import 'package:dio/dio.dart';
import '../config/env.dart';
import 'interceptors/auth_interceptor.dart';
import 'interceptors/logger_interceptor.dart';
class DioClient {
late final Dio _dio;
DioClient() {
_dio = Dio(
BaseOptions(
baseUrl: Env.apiBaseUrl,
connectTimeout: Duration(seconds: 30),
receiveTimeout: Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
),
);
_dio.interceptors.addAll([
AuthInterceptor(),
LoggerInterceptor(),
]);
}
Dio get dio => _dio;
}
// lib/features/auth/presentation/providers/auth_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../data/repositories/auth_repository_impl.dart';
import '../../domain/entities/user.dart';
import '../../domain/usecases/login_usecase.dart';
final authStateProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
final loginUseCase = ref.watch(loginUseCaseProvider);
return AuthNotifier(loginUseCase);
});
class AuthNotifier extends StateNotifier<AuthState> {
final LoginUseCase _loginUseCase;
AuthNotifier(this._loginUseCase) : super(AuthState.initial());
Future<void> login(String email, String password) async {
state = state.copyWith(isLoading: true);
final result = await _loginUseCase(email, password);
result.fold(
(failure) => state = state.copyWith(
isLoading: false,
error: failure.message,
),
(user) => state = state.copyWith(
isLoading: false,
user: user,
isAuthenticated: true,
),
);
}
}
class AuthState {
final bool isLoading;
final bool isAuthenticated;
final User? user;
final String? error;
AuthState({
required this.isLoading,
required this.isAuthenticated,
this.user,
this.error,
});
factory AuthState.initial() => AuthState(
isLoading: false,
isAuthenticated: false,
);
AuthState copyWith({
bool? isLoading,
bool? isAuthenticated,
User? user,
String? error,
}) {
return AuthState(
isLoading: isLoading ?? this.isLoading,
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
user: user ?? this.user,
error: error ?? this.error,
);
}
}
🐳 Docker & DevOps
docker-compose.yml # Dev services
Dockerfile # App container
.dockerignore
observability/
├── grafana/
│ ├── dashboards/
│ │ ├── api-performance.json
│ │ └── system-metrics.json
│ └── provisioning/
│ ├── datasources/
│ └── dashboards/
│
├── loki/
│ └── loki-config.yaml
│
├── tempo/
│ └── tempo-config.yaml
│
├── mimir/
│ └── mimir-config.yaml
│
└── promtail/
└── promtail-config.yaml
📚 Documentation
docs/
├── 00-getting-started/
├── 10-architecture/
├── 20-backend-go/
├── 30-frontend-flutter/
├── 40-database/
├── 50-observability-lgtm/
├── 60-devops-go/
├── 70-ci-cd/
├── 80-guides/
├── 90-features/
├── 95-technical-specs/
└── 99-changelog/
mint.json # Mintlify config
🔑 Key Files
Environment Files
- .env.example
- Taskfile.yml
- .air.toml
# Application
APP_ENV=development
APP_NAME=windsurf-app
APP_PORT=8080
# Database
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=secret
MYSQL_DATABASE=windsurf_db
MONGO_URI=mongodb://localhost:27017
MONGO_DATABASE=windsurf_mongo
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
# JWT
JWT_SECRET=your-secret-key
JWT_EXPIRY=24h
version: '3'
tasks:
dev:up:
desc: Start dev services
cmds:
- docker compose up -d
dev:down:
desc: Stop dev services
cmds:
- docker compose down -v
dev:air:
desc: Run backend with live reload
dir: backend
cmds:
- air
db:migrate:up:
desc: Run database migrations
dir: backend
cmds:
- go run cmd/migrate/main.go up
test:
desc: Run all tests
cmds:
- task: test:backend
- task: test:frontend
test:backend:
desc: Run backend tests
dir: backend
cmds:
- go test ./... -v -cover
test:frontend:
desc: Run frontend tests
dir: frontend
cmds:
- flutter test
# .air.toml
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd/api"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true
📊 Naming Conventions
Backend (Go)
- Packages: lowercase, single word (
handler,service,repository) - Files: snake_case (
user_handler.go,auth_service.go) - Types: PascalCase (
UserHandler,AuthService) - Functions: camelCase (exported:
NewUserHandler, private:validateInput) - Constants: UPPER_SNAKE_CASE atau PascalCase
- Interfaces: PascalCase dengan suffix (e.g.,
UserRepository,AuthService)
Frontend (Flutter)
- Files: snake_case (
login_page.dart,user_model.dart) - Classes: PascalCase (
LoginPage,UserModel) - Variables: camelCase (
userName,isLoading) - Constants: lowerCamelCase atau UPPER_SNAKE_CASE
- Private: prefix underscore (
_privateMethod,_PrivateClass)
🎯 Best Practices
Backend Structure
Backend Structure
- Gunakan Clean Architecture (handler → service → repository)
- Pisahkan domain logic dari infrastructure
- Setiap service punya folder sendiri di
internal/ - Shared code di
internal/shared/ataupkg/ - Test files sejajar dengan source files (
*_test.go)
Frontend Structure
Frontend Structure
- Gunakan Feature-First structure
- Setiap feature punya layer: data, domain, presentation
- Shared widgets di
shared/widgets/ - State management dengan Riverpod
- Dependency injection di
core/di/
Database Migrations
Database Migrations
- Naming:
{version}_{description}.{up|down}.sql - Selalu buat up & down migration
- Test migration sebelum commit
- Jangan edit migration yang sudah di-deploy
Configuration
Configuration
- Environment variables untuk secrets
- YAML files untuk app config
- Jangan commit
.envfile - Provide
.env.examplesebagai template
📚 Next Steps
Architecture Overview
Pahami arsitektur sistem secara keseluruhan
Backend Development
Mulai develop backend service
Frontend Development
Mulai develop Flutter app
DevOps Workflow
Setup development workflow
Pro Tip: Gunakan VS Code workspace atau GoLand multi-project untuk membuka backend dan frontend secara bersamaan!