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
Total: 82 endpoints
Adding New Routes
Create domain folder: internal/route/v1/your-domain/
Create route file: your_domain_route.go
Update route_registry.go with import and call
Build and test
See : 7-Domain Route Architecture
📱 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
Gunakan Clean Architecture (handler → service → repository)
Pisahkan domain logic dari infrastructure
Setiap service punya folder sendiri di internal/
Shared code di internal/shared/ atau pkg/
Test files sejajar dengan source files (*_test.go)
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/
Naming: {version}_{description}.{up|down}.sql
Selalu buat up & down migration
Test migration sebelum commit
Jangan edit migration yang sudah di-deploy
Environment variables untuk secrets
YAML files untuk app config
Jangan commit .env file
Provide .env.example sebagai template
📚 Next Steps
Pro Tip : Gunakan VS Code workspace atau GoLand multi-project untuk membuka backend dan frontend secara bersamaan!